use proc_macro::TokenStream;
use quote::{ToTokens, quote};
use syn::{
DeriveInput, Expr, GenericArgument, Lit, Meta, MetaNameValue, Path, PathArguments, Type,
parse_macro_input,
};
type ClapMcpAttrs = (
Option<bool>,
Option<bool>,
Option<bool>,
Option<bool>,
Option<bool>,
Option<bool>,
Option<bool>,
Option<String>,
Option<String>,
Option<String>,
);
fn meta_string_value(meta: &syn::meta::ParseNestedMeta) -> syn::Result<String> {
let value: Expr = meta.value()?.parse()?;
match value {
Expr::Lit(lit) => match lit.lit {
Lit::Str(s) => Ok(s.value()),
other => Err(meta.error(format!("expected string literal, got `{other:?}`"))),
},
other => Err(meta.error(format!("expected string literal, got `{other:?}`"))),
}
}
fn parse_clap_mcp_attrs(attrs: &[syn::Attribute]) -> ClapMcpAttrs {
let mut parallel_safe = None;
let mut reinvocation_safe = None;
let mut share_runtime = None;
let mut catch_in_process_panics = None;
let mut allow_mcp_without_subcommand = None;
let mut task_augmented_tools = None;
let mut stateful = None;
let mut mcp_flag = None;
let mut mcp_http_flag = None;
let mut export_skills_flag = None;
for attr in attrs {
if !attr.path().is_ident("clap_mcp") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("parallel_safe") {
if meta.input.peek(syn::token::Eq) {
let value: Expr = meta.value()?.parse()?;
parallel_safe = Some(expr_to_bool(&value));
} else {
parallel_safe = Some(true); }
} else if meta.path.is_ident("reinvocation_safe") {
if meta.input.peek(syn::token::Eq) {
let value: Expr = meta.value()?.parse()?;
reinvocation_safe = Some(expr_to_bool(&value));
} else {
reinvocation_safe = Some(true); }
} else if meta.path.is_ident("share_runtime") {
if meta.input.peek(syn::token::Eq) {
let value: Expr = meta.value()?.parse()?;
share_runtime = Some(expr_to_bool(&value));
} else {
share_runtime = Some(true); }
} else if meta.path.is_ident("catch_in_process_panics") {
if meta.input.peek(syn::token::Eq) {
let value: Expr = meta.value()?.parse()?;
catch_in_process_panics = Some(expr_to_bool(&value));
} else {
catch_in_process_panics = Some(true); }
} else if meta.path.is_ident("allow_mcp_without_subcommand") {
if meta.input.peek(syn::token::Eq) {
let value: Expr = meta.value()?.parse()?;
allow_mcp_without_subcommand = Some(expr_to_bool(&value));
} else {
allow_mcp_without_subcommand = Some(true); }
} else if meta.path.is_ident("task_augmented_tools") {
if meta.input.peek(syn::token::Eq) {
let value: Expr = meta.value()?.parse()?;
task_augmented_tools = Some(expr_to_bool(&value));
} else {
task_augmented_tools = Some(true); }
} else if meta.path.is_ident("stateful") {
if meta.input.peek(syn::token::Eq) {
let value: Expr = meta.value()?.parse()?;
stateful = Some(expr_to_bool(&value));
} else {
stateful = Some(true);
}
} else if meta.path.is_ident("mcp_flag") {
mcp_flag = Some(meta_string_value(&meta)?);
} else if meta.path.is_ident("mcp_http_flag") {
mcp_http_flag = Some(meta_string_value(&meta)?);
} else if meta.path.is_ident("export_skills_flag") {
export_skills_flag = Some(meta_string_value(&meta)?);
} else if meta.path.is_ident("args_metadata") && meta.input.peek(syn::token::Eq) {
let value: Expr = meta.value()?.parse()?;
if !expr_to_bool(&value) {
return Err(meta.error("args_metadata only supports `true` or bare flag"));
}
}
Ok(())
});
}
(
parallel_safe,
reinvocation_safe,
share_runtime,
catch_in_process_panics,
allow_mcp_without_subcommand,
task_augmented_tools,
stateful,
mcp_flag,
mcp_http_flag,
export_skills_flag,
)
}
fn expr_to_bool(expr: &Expr) -> bool {
match expr {
Expr::Lit(lit) => match &lit.lit {
Lit::Bool(b) => b.value,
_ => false,
},
_ => false,
}
}
fn field_has_command_flatten(attrs: &[syn::Attribute]) -> bool {
for attr in attrs {
if !attr.path().is_ident("command") {
continue;
}
let mut has_flatten = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("flatten") {
has_flatten = true;
}
Ok(())
});
if has_flatten {
return true;
}
}
false
}
fn field_has_command_subcommand(attrs: &[syn::Attribute]) -> bool {
for attr in attrs {
if !attr.path().is_ident("command") {
continue;
}
let mut has_subcommand = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("subcommand") {
has_subcommand = true;
}
Ok(())
});
if has_subcommand {
return true;
}
}
false
}
fn has_clap_mcp_task(attrs: &[syn::Attribute]) -> bool {
for attr in attrs {
if !attr.path().is_ident("clap_mcp") {
continue;
}
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("task") {
found = true;
}
Ok(())
});
if found {
return true;
}
}
false
}
enum ClapMcpSerialized {
Tool,
Args(Vec<String>),
}
fn get_clap_mcp_serialized(attrs: &[syn::Attribute]) -> Option<ClapMcpSerialized> {
for attr in attrs {
if !attr.path().is_ident("clap_mcp") {
continue;
}
let mut result = None;
let parse_result = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("serialized") {
if meta.input.peek(syn::token::Eq) {
let value: Expr = meta.value()?.parse()?;
if let Expr::Lit(lit) = value
&& let Lit::Str(s) = &lit.lit
{
let args: Vec<String> = s
.value()
.split(',')
.map(|p| p.trim().to_string())
.filter(|p| !p.is_empty())
.collect();
if args.is_empty() {
return Err(
meta.error("serialized = \"...\" requires at least one arg id")
);
}
result = Some(ClapMcpSerialized::Args(args));
}
} else {
result = Some(ClapMcpSerialized::Tool);
}
}
Ok(())
});
if parse_result.is_err() {
continue;
}
if result.is_some() {
return result;
}
}
None
}
fn clap_arg_id_from_field(ident: &syn::Ident, attrs: &[syn::Attribute]) -> String {
for attr in attrs {
if !attr.path().is_ident("arg") {
continue;
}
let mut id = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("id") {
let value: Expr = meta.value()?.parse()?;
if let Expr::Lit(lit) = value
&& let Lit::Str(s) = &lit.lit
{
id = Some(s.value());
}
}
Ok(())
});
if let Some(id) = id {
return id;
}
}
ident.to_string()
}
fn variant_field_ids(fields: &syn::Fields) -> Vec<String> {
fields
.iter()
.enumerate()
.map(|(i, f)| {
f.ident
.as_ref()
.map(|ident| clap_arg_id_from_field(ident, &f.attrs))
.unwrap_or_else(|| format!("__f{i}"))
})
.collect()
}
fn has_clap_mcp_serialize_topic(attrs: &[syn::Attribute]) -> bool {
for attr in attrs {
if !attr.path().is_ident("clap_mcp") {
continue;
}
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("serialize_topic") {
found = true;
}
Ok(())
});
if found {
return true;
}
}
false
}
fn serialized_scope_arg_ids(serialized: &ClapMcpSerialized) -> Option<&[String]> {
match serialized {
ClapMcpSerialized::Tool => None,
ClapMcpSerialized::Args(ids) => Some(ids.as_slice()),
}
}
fn has_clap_mcp_schema_only(attrs: &[syn::Attribute]) -> bool {
for attr in attrs {
if !attr.path().is_ident("clap_mcp") {
continue;
}
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("schema_only") {
found = true;
}
Ok(())
});
if found {
return true;
}
}
false
}
enum ClapMcpSkipMode {
None,
Bare,
Explicit(Vec<String>),
}
fn get_clap_mcp_skip_mode(attrs: &[syn::Attribute]) -> ClapMcpSkipMode {
for attr in attrs {
if !attr.path().is_ident("clap_mcp") {
continue;
}
let mut mode = ClapMcpSkipMode::None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("skip") {
if meta.input.peek(syn::token::Eq) {
let value: Expr = meta.value()?.parse()?;
if let Expr::Lit(lit) = value
&& let Lit::Str(s) = &lit.lit
{
mode = ClapMcpSkipMode::Explicit(
s.value()
.split(',')
.map(|p| p.trim().to_string())
.filter(|p| !p.is_empty())
.collect(),
);
}
} else {
mode = ClapMcpSkipMode::Bare;
}
}
Ok(())
});
if !matches!(mode, ClapMcpSkipMode::None) {
return mode;
}
}
ClapMcpSkipMode::None
}
fn has_clap_mcp_skip(attrs: &[syn::Attribute]) -> bool {
!matches!(get_clap_mcp_skip_mode(attrs), ClapMcpSkipMode::None)
}
fn has_clap_mcp_args_metadata(attrs: &[syn::Attribute]) -> bool {
for attr in attrs {
if !attr.path().is_ident("clap_mcp") {
continue;
}
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("args_metadata") {
found = true;
}
Ok(())
});
if found {
return true;
}
}
false
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum FlattenSkipKindTag {
Args,
Subcommand,
}
struct FlattenSkipEntry {
flat_ty: syn::Type,
root_name: String,
explicit: Option<Vec<String>>,
run_bare_probe: bool,
kind: FlattenSkipKindTag,
}
fn flattened_type_kind(ty: &syn::Type) -> Result<FlattenSkipKindTag, syn::Error> {
let ty = inner_type_if_option(ty).unwrap_or(ty);
let syn::Type::Path(type_path) = ty else {
return Err(syn::Error::new_spanned(
ty,
"clap_mcp: #[clap_mcp(skip)] on #[command(flatten)] requires a path type",
));
};
if type_path.path.segments.len() >= 2 {
let first = type_path.path.segments.first().unwrap();
let first_name = first.ident.to_string();
if first_name != "crate" && first_name != "self" && first_name != "super" {
return Err(syn::Error::new_spanned(
ty,
"clap_mcp: cannot classify external flattened type for skip; use imperative \
skip_commands / skip_args or define the type in this crate",
));
}
}
let last = type_path
.path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default();
let lower = last.to_ascii_lowercase();
let kind = if lower.contains("subcommand") || lower.ends_with("commands") {
FlattenSkipKindTag::Subcommand
} else {
FlattenSkipKindTag::Args
};
Ok(kind)
}
fn quote_flatten_skip_stmts(
entries: &[FlattenSkipEntry],
target: &proc_macro2::Ident,
) -> Vec<proc_macro2::TokenStream> {
entries
.iter()
.map(|entry| {
let flat_ty = &entry.flat_ty;
let root_lit = syn::LitStr::new(&entry.root_name, proc_macro2::Span::call_site());
let explicit_expr = match &entry.explicit {
Some(ids) => {
let lits = ids.iter().map(|s| {
let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
quote! { #lit.to_string() }
});
quote! { Some::<Vec<String>>(vec![#(#lits),*]) }
}
None => quote! { None::<Vec<String>> },
};
let run_bare_probe = entry.run_bare_probe;
let apply_fn = match entry.kind {
FlattenSkipKindTag::Args => {
quote! { clap_mcp::apply_flatten_args_field_skip::<#flat_ty> }
}
FlattenSkipKindTag::Subcommand => {
quote! { clap_mcp::apply_flatten_subcommand_field_skip::<#flat_ty> }
}
};
quote! {
#apply_fn(
&mut #target.skip_commands,
&mut #target.skip_args,
#root_lit,
#explicit_expr.as_deref(),
#run_bare_probe,
);
}
})
.collect()
}
fn apply_field_skip(
skip_args: &mut std::collections::HashMap<String, Vec<String>>,
flatten_skip_entries: &mut Vec<FlattenSkipEntry>,
root_name: &str,
arg_id: &str,
field_attrs: &[syn::Attribute],
field_ty: &syn::Type,
) -> Option<syn::Error> {
match get_clap_mcp_skip_mode(field_attrs) {
ClapMcpSkipMode::None => {}
ClapMcpSkipMode::Bare => {
if field_has_command_subcommand(field_attrs) {
let sub_ty = inner_type_if_option(field_ty).unwrap_or(field_ty).clone();
flatten_skip_entries.push(FlattenSkipEntry {
flat_ty: sub_ty,
root_name: root_name.to_string(),
explicit: None,
run_bare_probe: true,
kind: FlattenSkipKindTag::Subcommand,
});
} else if field_has_command_flatten(field_attrs) {
let flat_ty = inner_type_if_option(field_ty).unwrap_or(field_ty).clone();
let kind = match flattened_type_kind(&flat_ty) {
Ok(k) => k,
Err(e) => return Some(e),
};
flatten_skip_entries.push(FlattenSkipEntry {
flat_ty,
root_name: root_name.to_string(),
explicit: None,
run_bare_probe: true,
kind,
});
} else {
skip_args
.entry(root_name.to_string())
.or_default()
.push(arg_id.to_string());
}
}
ClapMcpSkipMode::Explicit(ids) => {
if field_has_command_subcommand(field_attrs) {
let sub_ty = inner_type_if_option(field_ty).unwrap_or(field_ty).clone();
flatten_skip_entries.push(FlattenSkipEntry {
flat_ty: sub_ty,
root_name: root_name.to_string(),
explicit: Some(ids),
run_bare_probe: false,
kind: FlattenSkipKindTag::Subcommand,
});
} else if field_has_command_flatten(field_attrs) {
let flat_ty = inner_type_if_option(field_ty).unwrap_or(field_ty).clone();
let kind = match flattened_type_kind(&flat_ty) {
Ok(k) => k,
Err(e) => return Some(e),
};
flatten_skip_entries.push(FlattenSkipEntry {
flat_ty,
root_name: root_name.to_string(),
explicit: Some(ids),
run_bare_probe: true,
kind,
});
} else {
skip_args
.entry(root_name.to_string())
.or_default()
.extend(ids);
}
}
}
None
}
fn build_args_metadata_impl(input: &DeriveInput) -> proc_macro2::TokenStream {
let name = &input.ident;
let syn::Data::Struct(data) = &input.data else {
return syn::Error::new_spanned(
name,
"clap_mcp: #[clap_mcp(args_metadata)] is only supported on structs",
)
.to_compile_error();
};
let mut field_ids: Vec<String> = Vec::new();
let mut topic_entries: Vec<(String, syn::Type)> = Vec::new();
let mut flatten_merge_stmts: Vec<proc_macro2::TokenStream> = Vec::new();
let mut nested_flatten_types: Vec<syn::Type> = Vec::new();
for (i, f) in data.fields.iter().enumerate() {
let arg_id = f
.ident
.as_ref()
.map(|ident| clap_arg_id_from_field(ident, &f.attrs))
.unwrap_or_else(|| format!("__f{i}"));
if field_has_command_flatten(&f.attrs) {
let flat_ty = inner_type_if_option(&f.ty).unwrap_or(&f.ty).clone();
nested_flatten_types.push(flat_ty.clone());
flatten_merge_stmts.push(quote! {
<#flat_ty as clap_mcp::ClapMcpFlattenArgsTopics>::merge_serialize_topics(
tool_name,
target,
);
});
continue;
}
field_ids.push(arg_id.clone());
if has_clap_mcp_serialize_topic(&f.attrs) {
topic_entries.push((arg_id, f.ty.clone()));
}
}
let field_id_lits = field_ids.iter().map(|s| {
let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
quote! { #lit }
});
let merge_entries = topic_entries.iter().map(|(arg_id, ty)| {
let arg_lit = syn::LitStr::new(arg_id, proc_macro2::Span::call_site());
quote! {
target
.entry(tool_name.to_string())
.or_default()
.insert(
#arg_lit.to_string(),
<#ty as clap_mcp::ClapMcpSerializeTopic>::serialize_topic_segment,
);
}
});
let nested_field_ids_expr = if nested_flatten_types.len() == 1 {
let ty = &nested_flatten_types[0];
quote! { <#ty as clap_mcp::ClapMcpFlattenArgsTopics>::FIELD_IDS }
} else {
quote! { &[] as &[&str] }
};
quote! {
impl clap_mcp::ClapMcpFlattenArgsTopics for #name {
const FIELD_IDS: &'static [&'static str] = &[#(#field_id_lits),*];
const NESTED_FIELD_IDS: &'static [&'static str] = #nested_field_ids_expr;
fn merge_serialize_topics(
tool_name: &str,
target: &mut std::collections::HashMap<
String,
std::collections::HashMap<String, clap_mcp::SerializeTopicSegmentFn>,
>,
) {
#(#merge_entries)*
#(#flatten_merge_stmts)*
}
}
impl clap_mcp::ClapMcpConfigProvider for #name {
fn clap_mcp_config() -> clap_mcp::ClapMcpConfig {
clap_mcp::ClapMcpConfig::default()
}
}
impl clap_mcp::ClapMcpSchemaMetadataProvider for #name {
fn clap_mcp_schema_metadata() -> clap_mcp::ClapMcpSchemaMetadata {
clap_mcp::ClapMcpSchemaMetadata::default()
}
}
}
}
fn has_clap_mcp_skip_root_when_subcommands(attrs: &[syn::Attribute]) -> bool {
for attr in attrs {
if !attr.path().is_ident("clap_mcp") {
continue;
}
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("skip_root_when_subcommands") {
found = true;
}
Ok(())
});
if found {
return true;
}
}
false
}
fn get_clap_mcp_requires_variant(attrs: &[syn::Attribute]) -> Option<Vec<String>> {
for attr in attrs {
if !attr.path().is_ident("clap_mcp") {
continue;
}
let mut result = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("requires") && meta.input.peek(syn::token::Eq) {
let value: Expr = meta.value()?.parse()?;
if let Expr::Lit(lit) = value
&& let Lit::Str(s) = &lit.lit
{
result = Some(
s.value()
.split(',')
.map(|p| p.trim().to_string())
.filter(|p| !p.is_empty())
.collect(),
);
}
}
Ok(())
});
if result.is_some() {
return result;
}
}
None
}
fn get_clap_mcp_output_from(attrs: &[syn::Attribute]) -> Option<Path> {
for attr in attrs {
if !attr.path().is_ident("clap_mcp_output_from") {
continue;
}
if let Meta::NameValue(MetaNameValue { value, .. }) = &attr.meta
&& let Expr::Lit(lit) = value
&& let Lit::Str(s) = &lit.lit
&& let Ok(path) = syn::parse_str::<Path>(&s.value())
{
return Some(path);
}
}
None
}
fn get_clap_mcp_output_from_with_state(attrs: &[syn::Attribute]) -> Option<Path> {
for attr in attrs {
if !attr.path().is_ident("clap_mcp_output_from_with_state") {
continue;
}
if let Meta::NameValue(MetaNameValue { value, .. }) = &attr.meta
&& let Expr::Lit(lit) = value
&& let Lit::Str(s) = &lit.lit
&& let Ok(path) = syn::parse_str::<Path>(&s.value())
{
return Some(path);
}
}
None
}
fn get_clap_mcp_state_type(attrs: &[syn::Attribute]) -> Option<syn::Type> {
for attr in attrs {
if !attr.path().is_ident("clap_mcp_state_type") {
continue;
}
if let Meta::NameValue(MetaNameValue { value, .. }) = &attr.meta
&& let Expr::Lit(lit) = value
&& let Lit::Str(s) = &lit.lit
&& let Ok(ty) = syn::parse_str::<syn::Type>(&s.value())
{
return Some(ty);
}
}
None
}
fn get_clap_mcp_output_type(attrs: &[syn::Attribute]) -> Option<syn::Type> {
for attr in attrs {
if !attr.path().is_ident("clap_mcp_output_type") {
continue;
}
if let Meta::NameValue(MetaNameValue { value, .. }) = &attr.meta
&& let Expr::Lit(lit) = value
&& let Lit::Str(s) = &lit.lit
&& let Ok(ty) = syn::parse_str::<syn::Type>(&s.value())
{
return Some(ty);
}
}
None
}
fn get_clap_mcp_output_one_of(attrs: &[syn::Attribute]) -> Option<Vec<syn::Type>> {
for attr in attrs {
if !attr.path().is_ident("clap_mcp_output_one_of") {
continue;
}
if let Meta::NameValue(MetaNameValue { value, .. }) = &attr.meta
&& let Expr::Lit(lit) = value
&& let Lit::Str(s) = &lit.lit
{
let types: Result<Vec<syn::Type>, _> = s
.value()
.split(',')
.map(|p| syn::parse_str::<syn::Type>(p.trim()))
.collect();
return types.ok();
}
}
None
}
fn get_clap_mcp_requires(attrs: &[syn::Attribute]) -> Option<String> {
for attr in attrs {
if !attr.path().is_ident("clap_mcp") {
continue;
}
let mut result = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("requires") {
if meta.input.peek(syn::token::Eq) {
let value: Expr = meta.value()?.parse()?;
if let Expr::Lit(lit) = value
&& let Lit::Str(s) = &lit.lit
{
result = Some(s.value());
}
} else {
result = Some(String::new()); }
}
Ok(())
});
if result.is_some() {
return result;
}
}
None
}
fn field_has_arg_long_or_short(attrs: &[syn::Attribute]) -> bool {
for attr in attrs {
if !attr.path().is_ident("arg") {
continue;
}
let mut has = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("long") || meta.path.is_ident("short") {
has = true;
}
Ok(())
});
if has {
return true;
}
}
false
}
fn field_has_arg_index(attrs: &[syn::Attribute]) -> bool {
for attr in attrs {
if !attr.path().is_ident("arg") {
continue;
}
let mut has = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("index") {
has = true;
}
Ok(())
});
if has {
return true;
}
}
false
}
fn field_looks_positional(attrs: &[syn::Attribute]) -> bool {
field_has_arg_index(attrs) || !field_has_arg_long_or_short(attrs)
}
fn get_command_name(attrs: &[syn::Attribute], ident: &syn::Ident) -> String {
for attr in attrs {
if !attr.path().is_ident("command") {
continue;
}
let mut name = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
let value: Expr = meta.value()?.parse()?;
if let Expr::Lit(lit) = value
&& let Lit::Str(s) = &lit.lit
{
name = Some(s.value());
}
}
Ok(())
});
if let Some(n) = name {
return n;
}
}
ident_to_kebab(ident)
}
fn inner_type_if_option(ty: &Type) -> Option<&Type> {
let Type::Path(type_path) = ty else {
return None;
};
let last = type_path.path.segments.last()?;
if last.ident != "Option" {
return None;
}
let PathArguments::AngleBracketed(args) = &last.arguments else {
return None;
};
args.args.first().and_then(|a| {
if let GenericArgument::Type(t) = a {
Some(t)
} else {
None
}
})
}
fn ident_to_kebab(ident: &syn::Ident) -> String {
let s = ident.to_string();
let mut out = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() && i > 0 {
out.push('-');
}
for c in c.to_lowercase() {
out.push(c);
}
}
out
}
fn is_option_type(ty: &Type) -> bool {
let Type::Path(type_path) = ty else {
return false;
};
let Some(last) = type_path.path.segments.last() else {
return false;
};
if last.ident != "Option" {
return false;
}
let PathArguments::AngleBracketed(args) = &last.arguments else {
return false;
};
let type_args: Vec<_> = args
.args
.iter()
.filter_map(|a| {
if let GenericArgument::Type(_) = a {
Some(())
} else {
None
}
})
.collect();
type_args.len() == 1
}
fn field_is_repeated_mcp_scalar(ty: &Type) -> bool {
is_positional_scalar_field(ty)
}
fn is_positional_scalar_field(ty: &Type) -> bool {
let ty = inner_type_if_option(ty).unwrap_or(ty);
let Type::Path(type_path) = ty else {
return true;
};
let Some(last) = type_path.path.segments.last() else {
return true;
};
if last.ident == "Vec" {
return false;
}
true
}
fn has_ambiguous_mcp_positionals<'a, I>(fields: I) -> bool
where
I: IntoIterator<Item = &'a syn::Field>,
{
let mut positional_scalars = 0usize;
for field in fields {
if has_clap_mcp_skip(&field.attrs)
|| !field_looks_positional(&field.attrs)
|| !field_is_repeated_mcp_scalar(&field.ty)
{
continue;
}
positional_scalars += 1;
if positional_scalars > 1 {
return true;
}
}
false
}
fn strip_option_type(ty: &Type) -> Type {
inner_type_if_option(ty)
.map(|t| (*t).clone())
.unwrap_or_else(|| ty.clone())
}
fn subcommand_field_type_from_enum(data: &syn::DataEnum) -> Option<Type> {
let mut found: Option<Type> = None;
for variant in &data.variants {
for field in variant.fields.iter() {
if !field_has_command_subcommand(&field.attrs) {
continue;
}
let ty = strip_option_type(&field.ty);
if let Some(prev) = &found {
if prev.to_token_stream().to_string() != ty.to_token_stream().to_string() {
return None;
}
} else {
found = Some(ty);
}
}
}
found
}
fn nested_subcommand_type_paths_from_enum(data: &syn::DataEnum) -> Vec<syn::Path> {
let mut paths = Vec::new();
let mut seen = std::collections::HashSet::new();
for variant in &data.variants {
for field in variant.fields.iter() {
if !field_has_command_subcommand(&field.attrs) {
continue;
}
let sub_ty = inner_type_if_option(&field.ty).unwrap_or(&field.ty);
if let syn::Type::Path(tp) = sub_ty {
let key = tp.path.to_token_stream().to_string();
if seen.insert(key) {
paths.push(tp.path.clone());
}
}
}
}
paths
}
#[proc_macro_derive(
ClapMcp,
attributes(
clap_mcp,
clap_mcp_output_from,
clap_mcp_output_from_with_state,
clap_mcp_state_type,
clap_mcp_output_type,
clap_mcp_output_one_of,
command,
arg
)
)]
pub fn derive_clap_mcp(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
if has_clap_mcp_args_metadata(&input.attrs) {
return TokenStream::from(build_args_metadata_impl(&input));
}
let mut serialized_flatten_const_checks: Vec<proc_macro2::TokenStream> = Vec::new();
match &input.data {
syn::Data::Enum(data) => {
for variant in &data.variants {
if has_clap_mcp_skip(&variant.attrs) {
continue;
}
if has_ambiguous_mcp_positionals(variant.fields.iter()) {
return TokenStream::from(
syn::Error::new_spanned(
&variant.ident,
"clap_mcp: multiple positional scalar arguments are ambiguous for MCP \
tool calls; use #[arg(long)] on each field or #[clap_mcp(skip)]",
)
.to_compile_error(),
);
}
if let Some(serialized) = get_clap_mcp_serialized(&variant.attrs) {
let field_ids: std::collections::HashSet<String> =
variant_field_ids(&variant.fields).into_iter().collect();
let flatten_types: Vec<syn::Type> = variant
.fields
.iter()
.filter(|f| field_has_command_flatten(&f.attrs))
.map(|f| inner_type_if_option(&f.ty).unwrap_or(&f.ty).clone())
.collect();
if let ClapMcpSerialized::Args(arg_ids) = &serialized {
for arg_id in arg_ids {
if field_ids.contains(arg_id) {
continue;
}
if flatten_types.is_empty() {
return TokenStream::from(
syn::Error::new_spanned(
&variant.ident,
format!(
"clap_mcp: serialized = \"{arg_id}\" — no field or arg \
with id `{arg_id}` on this variant"
),
)
.to_compile_error(),
);
}
let arg_lit = syn::LitStr::new(arg_id, proc_macro2::Span::call_site());
let checks = flatten_types.iter().map(|ty| {
quote! { clap_mcp::flatten_args_contains_field::<#ty>(#arg_lit) }
});
serialized_flatten_const_checks.push(quote! {
const _: () = {
clap_mcp::assert_serialized_in_any_flatten_args(
#arg_lit,
&[#(#checks),*],
);
};
});
}
}
for (i, f) in variant.fields.iter().enumerate() {
if !has_clap_mcp_serialize_topic(&f.attrs) {
continue;
}
let arg_id = f
.ident
.as_ref()
.map(|ident| clap_arg_id_from_field(ident, &f.attrs))
.unwrap_or_else(|| format!("__f{i}"));
let Some(scope_args) = serialized_scope_arg_ids(&serialized) else {
return TokenStream::from(
syn::Error::new_spanned(
f.ident.as_ref().unwrap_or(&variant.ident),
"clap_mcp: #[clap_mcp(serialize_topic)] requires arg-scoped \
#[clap_mcp(serialized = \"arg_id\")] on the same variant",
)
.to_compile_error(),
);
};
if !scope_args.contains(&arg_id) {
return TokenStream::from(
syn::Error::new_spanned(
f.ident.as_ref().unwrap_or(&variant.ident),
format!(
"clap_mcp: #[clap_mcp(serialize_topic)] on `{arg_id}` \
requires that arg in #[clap_mcp(serialized = \"...\")]"
),
)
.to_compile_error(),
);
}
}
}
}
}
syn::Data::Struct(data) => {
let subcommand_field = data
.fields
.iter()
.find(|f| field_has_command_subcommand(&f.attrs));
if has_ambiguous_mcp_positionals(
data.fields
.iter()
.filter(|f| !subcommand_field.is_some_and(|sf| std::ptr::eq(sf, *f))),
) {
return TokenStream::from(
syn::Error::new_spanned(
&input.ident,
"clap_mcp: multiple positional scalar arguments are ambiguous for MCP \
tool calls; use #[arg(long)] on each field or #[clap_mcp(skip)]",
)
.to_compile_error(),
);
}
}
_ => {}
}
let output_from_with_state = get_clap_mcp_output_from_with_state(&input.attrs);
let state_type = get_clap_mcp_state_type(&input.attrs);
let schema_only = has_clap_mcp_schema_only(&input.attrs);
if schema_only {
if !matches!(&input.data, syn::Data::Enum(_)) {
return TokenStream::from(
syn::Error::new_spanned(
&input.ident,
"clap_mcp: #[clap_mcp(schema_only)] is only supported on subcommand enums",
)
.to_compile_error(),
);
}
if get_clap_mcp_output_from(&input.attrs).is_some()
|| output_from_with_state.is_some()
|| get_clap_mcp_state_type(&input.attrs).is_some()
{
return TokenStream::from(
syn::Error::new_spanned(
&input.ident,
"clap_mcp: #[clap_mcp(schema_only)] cannot be combined with \
#[clap_mcp_output_from], #[clap_mcp_output_from_with_state], or \
#[clap_mcp_state_type]",
)
.to_compile_error(),
);
}
}
if state_type.is_some() && output_from_with_state.is_none() {
return TokenStream::from(
syn::Error::new_spanned(
&input.ident,
"clap_mcp: #[clap_mcp_state_type = \"Type\"] requires \
#[clap_mcp_output_from_with_state = \"run\"] on the same type",
)
.to_compile_error(),
);
}
let name = &input.ident;
let (
parallel_safe,
reinvocation_safe,
share_runtime,
catch_in_process_panics,
allow_mcp_without_subcommand,
task_augmented_tools,
stateful,
mcp_flag,
mcp_http_flag,
export_skills_flag,
) = parse_clap_mcp_attrs(&input.attrs);
let stateful_effective = stateful.unwrap_or(false);
let reinvocation_effective = reinvocation_safe.unwrap_or(false);
if task_augmented_tools == Some(true) && !reinvocation_effective {
return TokenStream::from(
syn::Error::new(
proc_macro2::Span::call_site(),
"clap_mcp: task_augmented_tools requires reinvocation_safe (in-process execution); subprocess mode cannot use MCP task-augmented tools/call",
)
.to_compile_error(),
);
}
if output_from_with_state.is_some() && !reinvocation_effective {
return TokenStream::from(
syn::Error::new_spanned(
&input.ident,
"clap_mcp: stateful MCP tools require reinvocation_safe (in-process execution)",
)
.to_compile_error(),
);
}
if stateful_effective && !reinvocation_effective {
return TokenStream::from(
syn::Error::new_spanned(
&input.ident,
"clap_mcp: #[clap_mcp(stateful)] requires reinvocation_safe (in-process execution)",
)
.to_compile_error(),
);
}
if schema_only && stateful_effective {
return TokenStream::from(
syn::Error::new_spanned(
&input.ident,
"clap_mcp: #[clap_mcp(schema_only)] cannot be combined with #[clap_mcp(stateful)]",
)
.to_compile_error(),
);
}
let parallel_safe_expr = parallel_safe
.map(|b| quote! { #b })
.unwrap_or_else(|| quote! { clap_mcp::ClapMcpConfig::default().parallel_safe });
let reinvocation_safe_expr = reinvocation_safe
.map(|b| quote! { #b })
.unwrap_or_else(|| quote! { clap_mcp::ClapMcpConfig::default().reinvocation_safe });
let share_runtime_expr = share_runtime
.map(|b| quote! { #b })
.unwrap_or_else(|| quote! { clap_mcp::ClapMcpConfig::default().share_runtime });
let catch_in_process_panics_expr = catch_in_process_panics
.map(|b| quote! { #b })
.unwrap_or_else(|| quote! { clap_mcp::ClapMcpConfig::default().catch_in_process_panics });
let allow_mcp_without_subcommand_expr = allow_mcp_without_subcommand
.map(|b| quote! { #b })
.unwrap_or_else(
|| quote! { clap_mcp::ClapMcpConfig::default().allow_mcp_without_subcommand },
);
let mut builtin_flag_stmts = Vec::new();
if let Some(long) = mcp_flag {
builtin_flag_stmts.push(quote! { flags = flags.with_stdio_long(#long); });
}
if let Some(long) = mcp_http_flag {
builtin_flag_stmts.push(quote! { flags = flags.with_http_long(#long); });
}
if let Some(long) = export_skills_flag {
builtin_flag_stmts.push(quote! { flags = flags.with_export_skills_long(#long); });
}
let builtin_flags_impl = if builtin_flag_stmts.is_empty() {
quote! { clap_mcp::ClapMcpBuiltinFlags::default() }
} else {
quote! {{
let mut flags = clap_mcp::ClapMcpBuiltinFlags::default();
#(#builtin_flag_stmts)*
flags
}}
};
let config_provider = quote! {
impl clap_mcp::ClapMcpConfigProvider for #name {
fn clap_mcp_config() -> clap_mcp::ClapMcpConfig {
clap_mcp::ClapMcpConfig {
parallel_safe: #parallel_safe_expr,
reinvocation_safe: #reinvocation_safe_expr,
share_runtime: #share_runtime_expr,
catch_in_process_panics: #catch_in_process_panics_expr,
allow_mcp_without_subcommand: #allow_mcp_without_subcommand_expr,
builtin_flags: #builtin_flags_impl,
}
}
}
};
let executor_impl = match &input.data {
syn::Data::Enum(data) => {
if schema_only {
quote! {}
} else {
let run_path = get_clap_mcp_output_from(&input.attrs);
let run_with_state = output_from_with_state.as_ref();
let state_ty = state_type.as_ref();
let projected_sub = subcommand_field_type_from_enum(data);
match (run_path, run_with_state, state_ty) {
(Some(run), None, None) => quote! {
impl clap_mcp::ClapMcpToolExecutor for #name {
fn execute_for_mcp(self) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
clap_mcp::IntoClapMcpResult::into_tool_result(#run(self))
}
}
},
(None, Some(run), Some(st)) => quote! {
impl clap_mcp::ClapMcpToolExecutorWithState for #name {
type State = #st;
fn execute_for_mcp_with_state(
self,
state: &Self::State,
) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
clap_mcp::IntoClapMcpResult::into_tool_result(#run(self, state))
}
}
},
(None, Some(run), None) => {
if let Some(sub_ty) = projected_sub {
quote! {
impl clap_mcp::ClapMcpToolExecutorWithState for #name {
type State = <#sub_ty as clap_mcp::ClapMcpToolExecutorWithState>::State;
fn execute_for_mcp_with_state(
self,
state: &Self::State,
) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
clap_mcp::IntoClapMcpResult::into_tool_result(#run(self, state))
}
}
}
} else {
let err = syn::Error::new_spanned(
&input.ident,
"clap_mcp: #[clap_mcp_output_from_with_state = \"run\"] on a leaf enum \
requires #[clap_mcp_state_type = \"Type\"] matching the second \
parameter of run (e.g. run(cmd, state: &Mutex<S>) → \
#[clap_mcp_state_type = \"Mutex<S>\"])",
);
return TokenStream::from(err.to_compile_error());
}
}
(Some(run), Some(run_st), Some(st)) => quote! {
impl clap_mcp::ClapMcpToolExecutor for #name {
fn execute_for_mcp(self) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
clap_mcp::IntoClapMcpResult::into_tool_result(#run(self))
}
}
impl clap_mcp::ClapMcpToolExecutorWithState for #name {
type State = #st;
fn execute_for_mcp_with_state(
self,
state: &Self::State,
) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
clap_mcp::IntoClapMcpResult::into_tool_result(#run_st(self, state))
}
}
},
_ => {
let err = syn::Error::new_spanned(
&input.ident,
"clap_mcp: enum must have #[clap_mcp_output_from = \"run\"] and/or \
#[clap_mcp_output_from_with_state = \"run\"] (with \
#[clap_mcp_state_type = \"Type\"] on leaf enums), or \
#[clap_mcp(schema_only)] when an ancestor owns tool execution",
);
return TokenStream::from(err.to_compile_error());
}
}
}
}
syn::Data::Struct(data) => {
let struct_run_path = get_clap_mcp_output_from(&input.attrs);
let subcommand_field = data
.fields
.iter()
.find(|f| field_has_command_subcommand(&f.attrs));
match subcommand_field {
Some(field) => {
if let Some(run) = struct_run_path {
if stateful_effective || output_from_with_state.is_some() {
let err = syn::Error::new_spanned(
&input.ident,
"clap_mcp: #[clap_mcp_output_from] on a struct root cannot be \
combined with #[clap_mcp(stateful)] or \
#[clap_mcp_output_from_with_state]; use subcommand delegation \
or a manual ClapMcpToolExecutor instead",
);
return TokenStream::from(err.to_compile_error());
}
quote! {
impl clap_mcp::ClapMcpToolExecutor for #name {
fn execute_for_mcp(self) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
clap_mcp::IntoClapMcpResult::into_tool_result(#run(self))
}
}
}
} else {
let field_ident = match &field.ident {
Some(id) => id.clone(),
None => {
let err = syn::Error::new_spanned(
field,
"clap_mcp: subcommand field must be named",
);
return TokenStream::from(err.to_compile_error());
}
};
let body = if is_option_type(&field.ty) {
quote! {
self.#field_ident.map_or_else(
|| Ok(clap_mcp::ClapMcpToolOutput::Text(String::new())),
|c| c.execute_for_mcp(),
)
}
} else {
quote! {
self.#field_ident.execute_for_mcp()
}
};
let state_body = if is_option_type(&field.ty) {
quote! {
self.#field_ident.map_or_else(
|| Ok(clap_mcp::ClapMcpToolOutput::Text(String::new())),
|c| c.execute_for_mcp_with_state(state),
)
}
} else {
quote! {
self.#field_ident.execute_for_mcp_with_state(state)
}
};
let mut impls = proc_macro2::TokenStream::new();
if !stateful_effective {
impls.extend(quote! {
impl clap_mcp::ClapMcpToolExecutor for #name {
fn execute_for_mcp(self) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
#body
}
}
});
} else {
let sub_ty = strip_option_type(&field.ty);
impls.extend(quote! {
impl clap_mcp::ClapMcpToolExecutorWithState for #name {
type State = <#sub_ty as clap_mcp::ClapMcpToolExecutorWithState>::State;
fn execute_for_mcp_with_state(
self,
state: &Self::State,
) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
#state_body
}
}
});
}
impls
}
}
None => {
if output_from_with_state.is_some() || stateful_effective {
let err = syn::Error::new_spanned(
&input.ident,
"clap_mcp: #[clap_mcp_output_from_with_state] and #[clap_mcp(stateful)] \
require a named subcommand field",
);
return TokenStream::from(err.to_compile_error());
}
if let Some(run) = struct_run_path {
quote! {
impl clap_mcp::ClapMcpToolExecutor for #name {
fn execute_for_mcp(self) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
clap_mcp::IntoClapMcpResult::into_tool_result(#run(self))
}
}
}
} else {
quote! {
impl clap_mcp::ClapMcpToolExecutor for #name {
fn execute_for_mcp(self) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
Ok(clap_mcp::ClapMcpToolOutput::Text(format!("{:?}", self)))
}
}
}
}
}
}
}
_ => quote! {
impl clap_mcp::ClapMcpToolExecutor for #name {
fn execute_for_mcp(self) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
Ok(clap_mcp::ClapMcpToolOutput::Text(format!("{:?}", self)))
}
}
},
};
let schema_metadata_impl = build_schema_metadata_impl(&input);
let expanded = quote! {
#(#serialized_flatten_const_checks)*
#config_provider
#executor_impl
#schema_metadata_impl
};
TokenStream::from(expanded)
}
fn serialize_topic_bindings_quote(
target: &syn::Ident,
bindings: &[(String, String, syn::Type)],
) -> proc_macro2::TokenStream {
let entries = bindings.iter().map(|(cmd, arg, ty)| {
let cmd_lit = syn::LitStr::new(cmd, proc_macro2::Span::call_site());
let arg_lit = syn::LitStr::new(arg, proc_macro2::Span::call_site());
quote! {
#target.serialize_topic_args
.entry(#cmd_lit.to_string())
.or_default()
.insert(
#arg_lit.to_string(),
<#ty as clap_mcp::ClapMcpSerializeTopic>::serialize_topic_segment,
);
}
});
quote! { #(#entries)* }
}
fn build_schema_metadata_impl(input: &DeriveInput) -> proc_macro2::TokenStream {
let name = &input.ident;
let (_, _, _, _, _, task_augmented_tools, _, _, _, _) = parse_clap_mcp_attrs(&input.attrs);
let task_augmented_tools_expr = task_augmented_tools
.map(|b| quote! { #b })
.unwrap_or(quote! { false });
let mut skip_commands = Vec::<String>::new();
let mut skip_args: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
let mut requires_args: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
let mut task_tool_names = Vec::<String>::new();
let mut serialize_tools: std::collections::HashMap<String, ClapMcpSerialized> =
std::collections::HashMap::new();
let mut serialize_topic_bindings: Vec<(String, String, syn::Type)> = Vec::new();
let mut flatten_serialize_topic_cmds: Vec<(String, syn::Type)> = Vec::new();
let mut flatten_skip_entries: Vec<FlattenSkipEntry> = Vec::new();
let mut flatten_skip_error: Option<syn::Error> = None;
let mut warn_optional_positional = false;
let optional_positional_warn_block: proc_macro2::TokenStream = quote! {
#[deprecated(note = "optional positional argument(s) without #[clap_mcp(requires)] or #[clap_mcp(skip)] may expose stdin to MCP; add one of these attributes for intentional behavior (see clap_mcp docs)")]
fn _clap_mcp_optional_positional_warn() {}
_clap_mcp_optional_positional_warn();
};
let output_schema_assign: proc_macro2::TokenStream =
if let Some(types) = get_clap_mcp_output_one_of(&input.attrs) {
if types.is_empty() {
quote! {}
} else {
quote! { m.output_schema = clap_mcp::output_schema_one_of!(#(#types),*); }
}
} else if let Some(ty) = get_clap_mcp_output_type(&input.attrs) {
quote! { m.output_schema = clap_mcp::output_schema_for_type::<#ty>(); }
} else {
quote! {}
};
match &input.data {
syn::Data::Enum(data) => {
for v in &data.variants {
let cmd_name = get_command_name(&v.attrs, &v.ident);
let variant_reqs = get_clap_mcp_requires_variant(&v.attrs).unwrap_or_default();
if has_clap_mcp_skip(&v.attrs) {
skip_commands.push(cmd_name.clone());
}
if has_clap_mcp_task(&v.attrs) {
task_tool_names.push(cmd_name.clone());
}
let variant_has_serialized_args = matches!(
get_clap_mcp_serialized(&v.attrs),
Some(ClapMcpSerialized::Args(_))
);
if let Some(serialized) = get_clap_mcp_serialized(&v.attrs) {
serialize_tools.insert(cmd_name.clone(), serialized);
}
requires_args
.entry(cmd_name.clone())
.or_default()
.extend(variant_reqs.clone());
for (i, f) in v.fields.iter().enumerate() {
let arg_id = f
.ident
.as_ref()
.map(|ident| clap_arg_id_from_field(ident, &f.attrs))
.unwrap_or_else(|| format!("__f{i}"));
if is_option_type(&f.ty)
&& field_looks_positional(&f.attrs)
&& !has_clap_mcp_skip(&f.attrs)
&& get_clap_mcp_requires(&f.attrs).is_none()
&& !variant_reqs.contains(&arg_id)
{
warn_optional_positional = true;
}
if let Some(e) = apply_field_skip(
&mut skip_args,
&mut flatten_skip_entries,
&cmd_name,
&arg_id,
&f.attrs,
&f.ty,
) {
flatten_skip_error = Some(e);
}
if field_has_command_flatten(&f.attrs) && variant_has_serialized_args {
let flat_ty = inner_type_if_option(&f.ty).unwrap_or(&f.ty).clone();
if matches!(flattened_type_kind(&flat_ty), Ok(FlattenSkipKindTag::Args)) {
flatten_serialize_topic_cmds.push((cmd_name.clone(), flat_ty));
}
}
if has_clap_mcp_serialize_topic(&f.attrs) {
serialize_topic_bindings.push((
cmd_name.clone(),
arg_id.clone(),
f.ty.clone(),
));
}
if let Some(req) = get_clap_mcp_requires(&f.attrs) {
let req_id = if req.is_empty() { arg_id } else { req };
requires_args
.entry(cmd_name.clone())
.or_default()
.push(req_id);
}
}
}
}
syn::Data::Struct(data) => {
let root_name = get_command_name(&input.attrs, name);
let subcommand_field = data
.fields
.iter()
.find(|f| field_has_command_subcommand(&f.attrs));
if let Some(sf) = subcommand_field
&& let Some(ref field_ident) = sf.ident
&& let Some(e) = apply_field_skip(
&mut skip_args,
&mut flatten_skip_entries,
&root_name,
&clap_arg_id_from_field(field_ident, &sf.attrs),
&sf.attrs,
&sf.ty,
)
{
flatten_skip_error = Some(e);
}
for f in &data.fields {
if subcommand_field.is_some_and(|sf| std::ptr::eq(sf, f)) {
continue;
}
let Some(ref field_ident) = f.ident else {
continue;
};
let arg_id = clap_arg_id_from_field(field_ident, &f.attrs);
if is_option_type(&f.ty)
&& field_looks_positional(&f.attrs)
&& !has_clap_mcp_skip(&f.attrs)
&& get_clap_mcp_requires(&f.attrs).is_none()
{
warn_optional_positional = true;
}
if let Some(e) = apply_field_skip(
&mut skip_args,
&mut flatten_skip_entries,
&root_name,
&arg_id,
&f.attrs,
&f.ty,
) {
flatten_skip_error = Some(e);
}
if let Some(req) = get_clap_mcp_requires(&f.attrs) {
let req_id = if req.is_empty() { arg_id } else { req };
requires_args
.entry(root_name.clone())
.or_default()
.push(req_id);
}
}
if let Some(sub_field) = subcommand_field {
let sub_ty = inner_type_if_option(&sub_field.ty).unwrap_or(&sub_field.ty);
if let syn::Type::Path(tp) = sub_ty {
let sub_path = &tp.path;
let skip_root_assign_local =
if has_clap_mcp_skip_root_when_subcommands(&input.attrs) {
quote! { local.skip_root_command_when_subcommands = true; }
} else {
quote! {}
};
let output_schema_assign_local: proc_macro2::TokenStream = if let Some(types) =
get_clap_mcp_output_one_of(&input.attrs)
{
if types.is_empty() {
quote! {}
} else {
quote! { local.output_schema = clap_mcp::output_schema_one_of!(#(#types),*); }
}
} else if let Some(ty) = get_clap_mcp_output_type(&input.attrs) {
quote! { local.output_schema = clap_mcp::output_schema_for_type::<#ty>(); }
} else {
quote! {}
};
let skip_root_assign = if has_clap_mcp_skip_root_when_subcommands(&input.attrs)
{
quote! { m.skip_root_command_when_subcommands = true; }
} else {
quote! {}
};
let merge = !skip_commands.is_empty()
|| !skip_args.is_empty()
|| !flatten_skip_entries.is_empty()
|| !requires_args.is_empty()
|| !task_tool_names.is_empty()
|| !serialize_tools.is_empty()
|| !serialize_topic_bindings.is_empty();
if merge {
let skip_commands_lit = skip_commands.iter().map(|s| {
let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
quote! { #lit.to_string() }
});
let task_tool_names_lit = task_tool_names.iter().map(|s| {
let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
quote! { #lit.to_string() }
});
let skip_args_entries = skip_args.iter().map(|(k, v)| {
let k_lit = syn::LitStr::new(k, proc_macro2::Span::call_site());
let vs = v
.iter()
.map(|s| {
let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
quote! { #lit.to_string() }
});
quote! {
local.skip_args.entry(#k_lit.to_string()).or_default().extend([#(#vs),*]);
}
});
let requires_args_entries = requires_args.iter().map(|(k, v)| {
let k_lit = syn::LitStr::new(k, proc_macro2::Span::call_site());
let vs = v
.iter()
.map(|s| {
let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
quote! { #lit.to_string() }
});
quote! {
local.requires_args.entry(#k_lit.to_string()).or_default().extend([#(#vs),*]);
}
});
let serialize_tools_entries = serialize_tools.iter().map(|(k, scope)| {
let k_lit = syn::LitStr::new(k, proc_macro2::Span::call_site());
match scope {
ClapMcpSerialized::Tool => quote! {
local.serialize_tools.insert(
#k_lit.to_string(),
clap_mcp::ClapMcpSerializeScope::Tool,
);
},
ClapMcpSerialized::Args(args) => {
let arg_lits = args.iter().map(|s| {
let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
quote! { #lit.to_string() }
});
quote! {
local.serialize_tools.insert(
#k_lit.to_string(),
clap_mcp::ClapMcpSerializeScope::Args(vec![#(#arg_lits),*]),
);
}
}
}
});
let serialize_topic_entries = serialize_topic_bindings_quote(
"e::format_ident!("local"),
&serialize_topic_bindings,
);
let flatten_skip_stmts_local = quote_flatten_skip_stmts(
&flatten_skip_entries,
"e::format_ident!("local"),
);
let flatten_topic_stmts_local = flatten_serialize_topic_cmds.iter().map(
|(cmd, ty)| {
let cmd_lit = syn::LitStr::new(cmd, proc_macro2::Span::call_site());
quote! {
<#ty as clap_mcp::ClapMcpFlattenArgsTopics>::merge_serialize_topics(
#cmd_lit,
&mut local.serialize_topic_args,
);
}
},
);
let warn_block = if warn_optional_positional {
optional_positional_warn_block.clone()
} else {
quote! {}
};
return quote! {
impl clap_mcp::ClapMcpSchemaMetadataProvider for #name {
fn clap_mcp_schema_metadata() -> clap_mcp::ClapMcpSchemaMetadata {
#warn_block
let mut m = <#sub_path as clap_mcp::ClapMcpSchemaMetadataProvider>::clap_mcp_schema_metadata();
let mut local = clap_mcp::ClapMcpSchemaMetadata::default();
local.skip_commands.extend([#(#skip_commands_lit),*]);
local.task_tool_names.extend([#(#task_tool_names_lit),*]);
local.task_augmented_tools = #task_augmented_tools_expr;
#(#skip_args_entries)*
#(#flatten_skip_stmts_local)*
#(#requires_args_entries)*
#(#serialize_tools_entries)*
#serialize_topic_entries
#(#flatten_topic_stmts_local)*
#skip_root_assign_local
#output_schema_assign_local
m.merge_from(local);
m
}
}
};
} else {
let flatten_skip_stmts_m = quote_flatten_skip_stmts(
&flatten_skip_entries,
"e::format_ident!("m"),
);
let warn_block = if warn_optional_positional {
optional_positional_warn_block.clone()
} else {
quote! {}
};
return quote! {
impl clap_mcp::ClapMcpSchemaMetadataProvider for #name {
fn clap_mcp_schema_metadata() -> clap_mcp::ClapMcpSchemaMetadata {
#warn_block
let mut m = <#sub_path as clap_mcp::ClapMcpSchemaMetadataProvider>::clap_mcp_schema_metadata();
m.task_augmented_tools = m.task_augmented_tools || #task_augmented_tools_expr;
#(#flatten_skip_stmts_m)*
#skip_root_assign
#output_schema_assign
m
}
}
};
}
}
}
}
_ => {}
}
if let Some(e) = flatten_skip_error {
return e.to_compile_error();
}
let skip_commands_lit = skip_commands.iter().map(|s| {
let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
quote! { #lit.to_string() }
});
let skip_args_entries = skip_args.iter().map(|(k, v)| {
let k_lit = syn::LitStr::new(k, proc_macro2::Span::call_site());
let vs = v.iter().map(|s| {
let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
quote! { #lit.to_string() }
});
quote! {
m.skip_args.insert(#k_lit.to_string(), vec![#(#vs),*]);
}
});
let requires_args_entries = requires_args.iter().map(|(k, v)| {
let k_lit = syn::LitStr::new(k, proc_macro2::Span::call_site());
let vs = v.iter().map(|s| {
let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
quote! { #lit.to_string() }
});
quote! {
m.requires_args.insert(#k_lit.to_string(), vec![#(#vs),*]);
}
});
let serialize_tools_entries = serialize_tools.iter().map(|(k, scope)| {
let k_lit = syn::LitStr::new(k, proc_macro2::Span::call_site());
match scope {
ClapMcpSerialized::Tool => quote! {
m.serialize_tools.insert(
#k_lit.to_string(),
clap_mcp::ClapMcpSerializeScope::Tool,
);
},
ClapMcpSerialized::Args(args) => {
let arg_lits = args.iter().map(|s| {
let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
quote! { #lit.to_string() }
});
quote! {
m.serialize_tools.insert(
#k_lit.to_string(),
clap_mcp::ClapMcpSerializeScope::Args(vec![#(#arg_lits),*]),
);
}
}
}
});
let serialize_topic_entries =
serialize_topic_bindings_quote("e::format_ident!("m"), &serialize_topic_bindings);
let flatten_skip_stmts =
quote_flatten_skip_stmts(&flatten_skip_entries, "e::format_ident!("m"));
let flatten_topic_stmts = flatten_serialize_topic_cmds.iter().map(|(cmd, ty)| {
let cmd_lit = syn::LitStr::new(cmd, proc_macro2::Span::call_site());
quote! {
<#ty as clap_mcp::ClapMcpFlattenArgsTopics>::merge_serialize_topics(
#cmd_lit,
&mut m.serialize_topic_args,
);
}
});
let task_tool_names_lit = task_tool_names.iter().map(|s| {
let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
quote! { #lit.to_string() }
});
let warn_block = if warn_optional_positional {
optional_positional_warn_block
} else {
quote! {}
};
let nested_merge_stmts = match &input.data {
syn::Data::Enum(data) => {
let paths = nested_subcommand_type_paths_from_enum(data);
paths.iter().map(|p| {
quote! { m.merge_from(<#p as clap_mcp::ClapMcpSchemaMetadataProvider>::clap_mcp_schema_metadata()); }
}).collect::<Vec<_>>()
}
_ => Vec::new(),
};
quote! {
impl clap_mcp::ClapMcpSchemaMetadataProvider for #name {
fn clap_mcp_schema_metadata() -> clap_mcp::ClapMcpSchemaMetadata {
#warn_block
let mut m = clap_mcp::ClapMcpSchemaMetadata::default();
#(#nested_merge_stmts)*
m.skip_commands.extend([#(#skip_commands_lit),*]);
m.task_tool_names.extend([#(#task_tool_names_lit),*]);
m.task_augmented_tools = m.task_augmented_tools || #task_augmented_tools_expr;
#(#skip_args_entries)*
#(#flatten_skip_stmts)*
#(#requires_args_entries)*
#(#serialize_tools_entries)*
#serialize_topic_entries
#(#flatten_topic_stmts)*
#output_schema_assign
m
}
}
}
}