Skip to main content

clap_mcp_macros/
lib.rs

1//! Procedural macros for clap-mcp.
2//!
3//! Provides `#[derive(ClapMcp)]` for attribute-based execution safety configuration
4//! and `ClapMcpToolExecutor` implementation.
5
6use proc_macro::TokenStream;
7use quote::{ToTokens, quote};
8use syn::{
9    DeriveInput, Expr, GenericArgument, Lit, Meta, MetaNameValue, Path, PathArguments, Type,
10    parse_macro_input,
11};
12
13/// Parsed `#[clap_mcp(...)]` config.
14type ClapMcpAttrs = (
15    Option<bool>,
16    Option<bool>,
17    Option<bool>,
18    Option<bool>,
19    Option<bool>,
20    Option<bool>,
21    Option<bool>,
22    Option<String>,
23    Option<String>,
24    Option<String>,
25);
26
27fn meta_string_value(meta: &syn::meta::ParseNestedMeta) -> syn::Result<String> {
28    let value: Expr = meta.value()?.parse()?;
29    match value {
30        Expr::Lit(lit) => match lit.lit {
31            Lit::Str(s) => Ok(s.value()),
32            other => Err(meta.error(format!("expected string literal, got `{other:?}`"))),
33        },
34        other => Err(meta.error(format!("expected string literal, got `{other:?}`"))),
35    }
36}
37
38/// Parses `#[clap_mcp(...)]` attributes.
39fn parse_clap_mcp_attrs(attrs: &[syn::Attribute]) -> ClapMcpAttrs {
40    let mut parallel_safe = None;
41    let mut reinvocation_safe = None;
42    let mut share_runtime = None;
43    let mut catch_in_process_panics = None;
44    let mut allow_mcp_without_subcommand = None;
45    let mut task_augmented_tools = None;
46    let mut stateful = None;
47    let mut mcp_flag = None;
48    let mut mcp_http_flag = None;
49    let mut export_skills_flag = None;
50
51    for attr in attrs {
52        if !attr.path().is_ident("clap_mcp") {
53            continue;
54        }
55
56        let _ = attr.parse_nested_meta(|meta| {
57            if meta.path.is_ident("parallel_safe") {
58                if meta.input.peek(syn::token::Eq) {
59                    let value: Expr = meta.value()?.parse()?;
60                    parallel_safe = Some(expr_to_bool(&value));
61                } else {
62                    parallel_safe = Some(true); // shorthand: parallel_safe means true
63                }
64            } else if meta.path.is_ident("reinvocation_safe") {
65                if meta.input.peek(syn::token::Eq) {
66                    let value: Expr = meta.value()?.parse()?;
67                    reinvocation_safe = Some(expr_to_bool(&value));
68                } else {
69                    reinvocation_safe = Some(true); // shorthand
70                }
71            } else if meta.path.is_ident("share_runtime") {
72                if meta.input.peek(syn::token::Eq) {
73                    let value: Expr = meta.value()?.parse()?;
74                    share_runtime = Some(expr_to_bool(&value));
75                } else {
76                    share_runtime = Some(true); // shorthand
77                }
78            } else if meta.path.is_ident("catch_in_process_panics") {
79                if meta.input.peek(syn::token::Eq) {
80                    let value: Expr = meta.value()?.parse()?;
81                    catch_in_process_panics = Some(expr_to_bool(&value));
82                } else {
83                    catch_in_process_panics = Some(true); // shorthand
84                }
85            } else if meta.path.is_ident("allow_mcp_without_subcommand") {
86                if meta.input.peek(syn::token::Eq) {
87                    let value: Expr = meta.value()?.parse()?;
88                    allow_mcp_without_subcommand = Some(expr_to_bool(&value));
89                } else {
90                    allow_mcp_without_subcommand = Some(true); // shorthand
91                }
92            } else if meta.path.is_ident("task_augmented_tools") {
93                if meta.input.peek(syn::token::Eq) {
94                    let value: Expr = meta.value()?.parse()?;
95                    task_augmented_tools = Some(expr_to_bool(&value));
96                } else {
97                    task_augmented_tools = Some(true); // shorthand
98                }
99            } else if meta.path.is_ident("stateful") {
100                if meta.input.peek(syn::token::Eq) {
101                    let value: Expr = meta.value()?.parse()?;
102                    stateful = Some(expr_to_bool(&value));
103                } else {
104                    stateful = Some(true);
105                }
106            } else if meta.path.is_ident("mcp_flag") {
107                mcp_flag = Some(meta_string_value(&meta)?);
108            } else if meta.path.is_ident("mcp_http_flag") {
109                mcp_http_flag = Some(meta_string_value(&meta)?);
110            } else if meta.path.is_ident("export_skills_flag") {
111                export_skills_flag = Some(meta_string_value(&meta)?);
112            } else if meta.path.is_ident("args_metadata") && meta.input.peek(syn::token::Eq) {
113                let value: Expr = meta.value()?.parse()?;
114                if !expr_to_bool(&value) {
115                    return Err(meta.error("args_metadata only supports `true` or bare flag"));
116                }
117            }
118            Ok(())
119        });
120    }
121
122    (
123        parallel_safe,
124        reinvocation_safe,
125        share_runtime,
126        catch_in_process_panics,
127        allow_mcp_without_subcommand,
128        task_augmented_tools,
129        stateful,
130        mcp_flag,
131        mcp_http_flag,
132        export_skills_flag,
133    )
134}
135
136fn expr_to_bool(expr: &Expr) -> bool {
137    match expr {
138        Expr::Lit(lit) => match &lit.lit {
139            Lit::Bool(b) => b.value,
140            _ => false,
141        },
142        _ => false,
143    }
144}
145
146/// Returns true if the field has `#[command(flatten)]`.
147fn field_has_command_flatten(attrs: &[syn::Attribute]) -> bool {
148    for attr in attrs {
149        if !attr.path().is_ident("command") {
150            continue;
151        }
152        let mut has_flatten = false;
153        let _ = attr.parse_nested_meta(|meta| {
154            if meta.path.is_ident("flatten") {
155                has_flatten = true;
156            }
157            Ok(())
158        });
159        if has_flatten {
160            return true;
161        }
162    }
163    false
164}
165
166/// Returns true if the field has `#[command(subcommand)]`.
167fn field_has_command_subcommand(attrs: &[syn::Attribute]) -> bool {
168    for attr in attrs {
169        if !attr.path().is_ident("command") {
170            continue;
171        }
172        let mut has_subcommand = false;
173        let _ = attr.parse_nested_meta(|meta| {
174            if meta.path.is_ident("subcommand") {
175                has_subcommand = true;
176            }
177            Ok(())
178        });
179        if has_subcommand {
180            return true;
181        }
182    }
183    false
184}
185
186/// Parses #[clap_mcp(task)] on enum variants — marks the tool as eligible for MCP task-augmented
187/// `tools/call` when [`ClapMcpConfig::task_augmented_tools`] is enabled. If no variant has this
188/// attribute, all tools are eligible when tasks are enabled.
189fn has_clap_mcp_task(attrs: &[syn::Attribute]) -> bool {
190    for attr in attrs {
191        if !attr.path().is_ident("clap_mcp") {
192            continue;
193        }
194        let mut found = false;
195        let _ = attr.parse_nested_meta(|meta| {
196            if meta.path.is_ident("task") {
197                found = true;
198            }
199            Ok(())
200        });
201        if found {
202            return true;
203        }
204    }
205    false
206}
207
208/// Parsed `#[clap_mcp(serialized)]` scope on a variant.
209enum ClapMcpSerialized {
210    Tool,
211    Args(Vec<String>),
212}
213
214/// Parses `#[clap_mcp(serialized)]` or `#[clap_mcp(serialized = "arg1, arg2")]` on enum variants.
215fn get_clap_mcp_serialized(attrs: &[syn::Attribute]) -> Option<ClapMcpSerialized> {
216    for attr in attrs {
217        if !attr.path().is_ident("clap_mcp") {
218            continue;
219        }
220        let mut result = None;
221        let parse_result = attr.parse_nested_meta(|meta| {
222            if meta.path.is_ident("serialized") {
223                if meta.input.peek(syn::token::Eq) {
224                    let value: Expr = meta.value()?.parse()?;
225                    if let Expr::Lit(lit) = value
226                        && let Lit::Str(s) = &lit.lit
227                    {
228                        let args: Vec<String> = s
229                            .value()
230                            .split(',')
231                            .map(|p| p.trim().to_string())
232                            .filter(|p| !p.is_empty())
233                            .collect();
234                        if args.is_empty() {
235                            return Err(
236                                meta.error("serialized = \"...\" requires at least one arg id")
237                            );
238                        }
239                        result = Some(ClapMcpSerialized::Args(args));
240                    }
241                } else {
242                    result = Some(ClapMcpSerialized::Tool);
243                }
244            }
245            Ok(())
246        });
247        if parse_result.is_err() {
248            continue;
249        }
250        if result.is_some() {
251            return result;
252        }
253    }
254    None
255}
256
257/// clap default: field ident; override via `#[arg(id = "...")]`.
258fn clap_arg_id_from_field(ident: &syn::Ident, attrs: &[syn::Attribute]) -> String {
259    for attr in attrs {
260        if !attr.path().is_ident("arg") {
261            continue;
262        }
263        let mut id = None;
264        let _ = attr.parse_nested_meta(|meta| {
265            if meta.path.is_ident("id") {
266                let value: Expr = meta.value()?.parse()?;
267                if let Expr::Lit(lit) = value
268                    && let Lit::Str(s) = &lit.lit
269                {
270                    id = Some(s.value());
271                }
272            }
273            Ok(())
274        });
275        if let Some(id) = id {
276            return id;
277        }
278    }
279    ident.to_string()
280}
281
282fn variant_field_ids(fields: &syn::Fields) -> Vec<String> {
283    fields
284        .iter()
285        .enumerate()
286        .map(|(i, f)| {
287            f.ident
288                .as_ref()
289                .map(|ident| clap_arg_id_from_field(ident, &f.attrs))
290                .unwrap_or_else(|| format!("__f{i}"))
291        })
292        .collect()
293}
294
295/// Parses `#[clap_mcp(serialize_topic)]` on a field used with arg-scoped `serialized`.
296fn has_clap_mcp_serialize_topic(attrs: &[syn::Attribute]) -> bool {
297    for attr in attrs {
298        if !attr.path().is_ident("clap_mcp") {
299            continue;
300        }
301        let mut found = false;
302        let _ = attr.parse_nested_meta(|meta| {
303            if meta.path.is_ident("serialize_topic") {
304                found = true;
305            }
306            Ok(())
307        });
308        if found {
309            return true;
310        }
311    }
312    false
313}
314
315fn serialized_scope_arg_ids(serialized: &ClapMcpSerialized) -> Option<&[String]> {
316    match serialized {
317        ClapMcpSerialized::Tool => None,
318        ClapMcpSerialized::Args(ids) => Some(ids.as_slice()),
319    }
320}
321
322/// Parses #[clap_mcp(skip)] from attributes.
323fn has_clap_mcp_schema_only(attrs: &[syn::Attribute]) -> bool {
324    for attr in attrs {
325        if !attr.path().is_ident("clap_mcp") {
326            continue;
327        }
328        let mut found = false;
329        let _ = attr.parse_nested_meta(|meta| {
330            if meta.path.is_ident("schema_only") {
331                found = true;
332            }
333            Ok(())
334        });
335        if found {
336            return true;
337        }
338    }
339    false
340}
341
342enum ClapMcpSkipMode {
343    None,
344    Bare,
345    Explicit(Vec<String>),
346}
347
348fn get_clap_mcp_skip_mode(attrs: &[syn::Attribute]) -> ClapMcpSkipMode {
349    for attr in attrs {
350        if !attr.path().is_ident("clap_mcp") {
351            continue;
352        }
353        let mut mode = ClapMcpSkipMode::None;
354        let _ = attr.parse_nested_meta(|meta| {
355            if meta.path.is_ident("skip") {
356                if meta.input.peek(syn::token::Eq) {
357                    let value: Expr = meta.value()?.parse()?;
358                    if let Expr::Lit(lit) = value
359                        && let Lit::Str(s) = &lit.lit
360                    {
361                        mode = ClapMcpSkipMode::Explicit(
362                            s.value()
363                                .split(',')
364                                .map(|p| p.trim().to_string())
365                                .filter(|p| !p.is_empty())
366                                .collect(),
367                        );
368                    }
369                } else {
370                    mode = ClapMcpSkipMode::Bare;
371                }
372            }
373            Ok(())
374        });
375        if !matches!(mode, ClapMcpSkipMode::None) {
376            return mode;
377        }
378    }
379    ClapMcpSkipMode::None
380}
381
382fn has_clap_mcp_skip(attrs: &[syn::Attribute]) -> bool {
383    !matches!(get_clap_mcp_skip_mode(attrs), ClapMcpSkipMode::None)
384}
385
386fn has_clap_mcp_args_metadata(attrs: &[syn::Attribute]) -> bool {
387    for attr in attrs {
388        if !attr.path().is_ident("clap_mcp") {
389            continue;
390        }
391        let mut found = false;
392        let _ = attr.parse_nested_meta(|meta| {
393            if meta.path.is_ident("args_metadata") {
394                found = true;
395            }
396            Ok(())
397        });
398        if found {
399            return true;
400        }
401    }
402    false
403}
404
405#[derive(Clone, Copy, PartialEq, Eq)]
406enum FlattenSkipKindTag {
407    Args,
408    Subcommand,
409}
410
411struct FlattenSkipEntry {
412    flat_ty: syn::Type,
413    root_name: String,
414    explicit: Option<Vec<String>>,
415    run_bare_probe: bool,
416    kind: FlattenSkipKindTag,
417}
418
419fn flattened_type_kind(ty: &syn::Type) -> Result<FlattenSkipKindTag, syn::Error> {
420    let ty = inner_type_if_option(ty).unwrap_or(ty);
421    let syn::Type::Path(type_path) = ty else {
422        return Err(syn::Error::new_spanned(
423            ty,
424            "clap_mcp: #[clap_mcp(skip)] on #[command(flatten)] requires a path type",
425        ));
426    };
427    if type_path.path.segments.len() >= 2 {
428        let first = type_path.path.segments.first().unwrap();
429        let first_name = first.ident.to_string();
430        if first_name != "crate" && first_name != "self" && first_name != "super" {
431            return Err(syn::Error::new_spanned(
432                ty,
433                "clap_mcp: cannot classify external flattened type for skip; use imperative \
434                 skip_commands / skip_args or define the type in this crate",
435            ));
436        }
437    }
438    let last = type_path
439        .path
440        .segments
441        .last()
442        .map(|s| s.ident.to_string())
443        .unwrap_or_default();
444    let lower = last.to_ascii_lowercase();
445    let kind = if lower.contains("subcommand") || lower.ends_with("commands") {
446        FlattenSkipKindTag::Subcommand
447    } else {
448        FlattenSkipKindTag::Args
449    };
450    Ok(kind)
451}
452
453fn quote_flatten_skip_stmts(
454    entries: &[FlattenSkipEntry],
455    target: &proc_macro2::Ident,
456) -> Vec<proc_macro2::TokenStream> {
457    entries
458        .iter()
459        .map(|entry| {
460            let flat_ty = &entry.flat_ty;
461            let root_lit = syn::LitStr::new(&entry.root_name, proc_macro2::Span::call_site());
462            let explicit_expr = match &entry.explicit {
463                Some(ids) => {
464                    let lits = ids.iter().map(|s| {
465                        let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
466                        quote! { #lit.to_string() }
467                    });
468                    quote! { Some::<Vec<String>>(vec![#(#lits),*]) }
469                }
470                None => quote! { None::<Vec<String>> },
471            };
472            let run_bare_probe = entry.run_bare_probe;
473            let apply_fn = match entry.kind {
474                FlattenSkipKindTag::Args => {
475                    quote! { clap_mcp::apply_flatten_args_field_skip::<#flat_ty> }
476                }
477                FlattenSkipKindTag::Subcommand => {
478                    quote! { clap_mcp::apply_flatten_subcommand_field_skip::<#flat_ty> }
479                }
480            };
481            quote! {
482                #apply_fn(
483                    &mut #target.skip_commands,
484                    &mut #target.skip_args,
485                    #root_lit,
486                    #explicit_expr.as_deref(),
487                    #run_bare_probe,
488                );
489            }
490        })
491        .collect()
492}
493
494fn apply_field_skip(
495    skip_args: &mut std::collections::HashMap<String, Vec<String>>,
496    flatten_skip_entries: &mut Vec<FlattenSkipEntry>,
497    root_name: &str,
498    arg_id: &str,
499    field_attrs: &[syn::Attribute],
500    field_ty: &syn::Type,
501) -> Option<syn::Error> {
502    match get_clap_mcp_skip_mode(field_attrs) {
503        ClapMcpSkipMode::None => {}
504        ClapMcpSkipMode::Bare => {
505            if field_has_command_subcommand(field_attrs) {
506                let sub_ty = inner_type_if_option(field_ty).unwrap_or(field_ty).clone();
507                flatten_skip_entries.push(FlattenSkipEntry {
508                    flat_ty: sub_ty,
509                    root_name: root_name.to_string(),
510                    explicit: None,
511                    run_bare_probe: true,
512                    kind: FlattenSkipKindTag::Subcommand,
513                });
514            } else if field_has_command_flatten(field_attrs) {
515                let flat_ty = inner_type_if_option(field_ty).unwrap_or(field_ty).clone();
516                let kind = match flattened_type_kind(&flat_ty) {
517                    Ok(k) => k,
518                    Err(e) => return Some(e),
519                };
520                flatten_skip_entries.push(FlattenSkipEntry {
521                    flat_ty,
522                    root_name: root_name.to_string(),
523                    explicit: None,
524                    run_bare_probe: true,
525                    kind,
526                });
527            } else {
528                skip_args
529                    .entry(root_name.to_string())
530                    .or_default()
531                    .push(arg_id.to_string());
532            }
533        }
534        ClapMcpSkipMode::Explicit(ids) => {
535            if field_has_command_subcommand(field_attrs) {
536                let sub_ty = inner_type_if_option(field_ty).unwrap_or(field_ty).clone();
537                flatten_skip_entries.push(FlattenSkipEntry {
538                    flat_ty: sub_ty,
539                    root_name: root_name.to_string(),
540                    explicit: Some(ids),
541                    run_bare_probe: false,
542                    kind: FlattenSkipKindTag::Subcommand,
543                });
544            } else if field_has_command_flatten(field_attrs) {
545                let flat_ty = inner_type_if_option(field_ty).unwrap_or(field_ty).clone();
546                let kind = match flattened_type_kind(&flat_ty) {
547                    Ok(k) => k,
548                    Err(e) => return Some(e),
549                };
550                flatten_skip_entries.push(FlattenSkipEntry {
551                    flat_ty,
552                    root_name: root_name.to_string(),
553                    explicit: Some(ids),
554                    run_bare_probe: true,
555                    kind,
556                });
557            } else {
558                skip_args
559                    .entry(root_name.to_string())
560                    .or_default()
561                    .extend(ids);
562            }
563        }
564    }
565    None
566}
567
568fn build_args_metadata_impl(input: &DeriveInput) -> proc_macro2::TokenStream {
569    let name = &input.ident;
570    let syn::Data::Struct(data) = &input.data else {
571        return syn::Error::new_spanned(
572            name,
573            "clap_mcp: #[clap_mcp(args_metadata)] is only supported on structs",
574        )
575        .to_compile_error();
576    };
577
578    let mut field_ids: Vec<String> = Vec::new();
579    let mut topic_entries: Vec<(String, syn::Type)> = Vec::new();
580    let mut flatten_merge_stmts: Vec<proc_macro2::TokenStream> = Vec::new();
581    let mut nested_flatten_types: Vec<syn::Type> = Vec::new();
582    for (i, f) in data.fields.iter().enumerate() {
583        let arg_id = f
584            .ident
585            .as_ref()
586            .map(|ident| clap_arg_id_from_field(ident, &f.attrs))
587            .unwrap_or_else(|| format!("__f{i}"));
588        if field_has_command_flatten(&f.attrs) {
589            let flat_ty = inner_type_if_option(&f.ty).unwrap_or(&f.ty).clone();
590            nested_flatten_types.push(flat_ty.clone());
591            flatten_merge_stmts.push(quote! {
592                <#flat_ty as clap_mcp::ClapMcpFlattenArgsTopics>::merge_serialize_topics(
593                    tool_name,
594                    target,
595                );
596            });
597            continue;
598        }
599        field_ids.push(arg_id.clone());
600        if has_clap_mcp_serialize_topic(&f.attrs) {
601            topic_entries.push((arg_id, f.ty.clone()));
602        }
603    }
604
605    let field_id_lits = field_ids.iter().map(|s| {
606        let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
607        quote! { #lit }
608    });
609    let merge_entries = topic_entries.iter().map(|(arg_id, ty)| {
610        let arg_lit = syn::LitStr::new(arg_id, proc_macro2::Span::call_site());
611        quote! {
612            target
613                .entry(tool_name.to_string())
614                .or_default()
615                .insert(
616                    #arg_lit.to_string(),
617                    <#ty as clap_mcp::ClapMcpSerializeTopic>::serialize_topic_segment,
618                );
619        }
620    });
621    let nested_field_ids_expr = if nested_flatten_types.len() == 1 {
622        let ty = &nested_flatten_types[0];
623        quote! { <#ty as clap_mcp::ClapMcpFlattenArgsTopics>::FIELD_IDS }
624    } else {
625        quote! { &[] as &[&str] }
626    };
627
628    quote! {
629        impl clap_mcp::ClapMcpFlattenArgsTopics for #name {
630            const FIELD_IDS: &'static [&'static str] = &[#(#field_id_lits),*];
631            const NESTED_FIELD_IDS: &'static [&'static str] = #nested_field_ids_expr;
632
633            fn merge_serialize_topics(
634                tool_name: &str,
635                target: &mut std::collections::HashMap<
636                    String,
637                    std::collections::HashMap<String, clap_mcp::SerializeTopicSegmentFn>,
638                >,
639            ) {
640                #(#merge_entries)*
641                #(#flatten_merge_stmts)*
642            }
643        }
644
645        impl clap_mcp::ClapMcpConfigProvider for #name {
646            fn clap_mcp_config() -> clap_mcp::ClapMcpConfig {
647                clap_mcp::ClapMcpConfig::default()
648            }
649        }
650
651        impl clap_mcp::ClapMcpSchemaMetadataProvider for #name {
652            fn clap_mcp_schema_metadata() -> clap_mcp::ClapMcpSchemaMetadata {
653                clap_mcp::ClapMcpSchemaMetadata::default()
654            }
655        }
656    }
657}
658
659/// Parses #[clap_mcp(skip_root_when_subcommands)] from root struct attributes.
660/// When present on a struct root with a subcommand, the root is excluded from the MCP tool list.
661fn has_clap_mcp_skip_root_when_subcommands(attrs: &[syn::Attribute]) -> bool {
662    for attr in attrs {
663        if !attr.path().is_ident("clap_mcp") {
664            continue;
665        }
666        let mut found = false;
667        let _ = attr.parse_nested_meta(|meta| {
668            if meta.path.is_ident("skip_root_when_subcommands") {
669                found = true;
670            }
671            Ok(())
672        });
673        if found {
674            return true;
675        }
676    }
677    false
678}
679
680/// Parses variant-level #[clap_mcp(requires = "arg1,arg2")] - comma-separated list.
681fn get_clap_mcp_requires_variant(attrs: &[syn::Attribute]) -> Option<Vec<String>> {
682    for attr in attrs {
683        if !attr.path().is_ident("clap_mcp") {
684            continue;
685        }
686        let mut result = None;
687        let _ = attr.parse_nested_meta(|meta| {
688            if meta.path.is_ident("requires") && meta.input.peek(syn::token::Eq) {
689                let value: Expr = meta.value()?.parse()?;
690                if let Expr::Lit(lit) = value
691                    && let Lit::Str(s) = &lit.lit
692                {
693                    result = Some(
694                        s.value()
695                            .split(',')
696                            .map(|p| p.trim().to_string())
697                            .filter(|p| !p.is_empty())
698                            .collect(),
699                    );
700                }
701            }
702            Ok(())
703        });
704        if result.is_some() {
705            return result;
706        }
707    }
708    None
709}
710
711/// Parses `#[clap_mcp_output_from = "run"]` (or path like `my_mod::run`) from enum attributes.
712/// When present, execute_for_mcp is generated by calling this function and converting the result.
713fn get_clap_mcp_output_from(attrs: &[syn::Attribute]) -> Option<Path> {
714    for attr in attrs {
715        if !attr.path().is_ident("clap_mcp_output_from") {
716            continue;
717        }
718        if let Meta::NameValue(MetaNameValue { value, .. }) = &attr.meta
719            && let Expr::Lit(lit) = value
720            && let Lit::Str(s) = &lit.lit
721            && let Ok(path) = syn::parse_str::<Path>(&s.value())
722        {
723            return Some(path);
724        }
725    }
726    None
727}
728
729fn get_clap_mcp_output_from_with_state(attrs: &[syn::Attribute]) -> Option<Path> {
730    for attr in attrs {
731        if !attr.path().is_ident("clap_mcp_output_from_with_state") {
732            continue;
733        }
734        if let Meta::NameValue(MetaNameValue { value, .. }) = &attr.meta
735            && let Expr::Lit(lit) = value
736            && let Lit::Str(s) = &lit.lit
737            && let Ok(path) = syn::parse_str::<Path>(&s.value())
738        {
739            return Some(path);
740        }
741    }
742    None
743}
744
745fn get_clap_mcp_state_type(attrs: &[syn::Attribute]) -> Option<syn::Type> {
746    for attr in attrs {
747        if !attr.path().is_ident("clap_mcp_state_type") {
748            continue;
749        }
750        if let Meta::NameValue(MetaNameValue { value, .. }) = &attr.meta
751            && let Expr::Lit(lit) = value
752            && let Lit::Str(s) = &lit.lit
753            && let Ok(ty) = syn::parse_str::<syn::Type>(&s.value())
754        {
755            return Some(ty);
756        }
757    }
758    None
759}
760
761/// Parses `#[clap_mcp_output_type = "TypeName"]` from enum attributes (for output schema).
762fn get_clap_mcp_output_type(attrs: &[syn::Attribute]) -> Option<syn::Type> {
763    for attr in attrs {
764        if !attr.path().is_ident("clap_mcp_output_type") {
765            continue;
766        }
767        if let Meta::NameValue(MetaNameValue { value, .. }) = &attr.meta
768            && let Expr::Lit(lit) = value
769            && let Lit::Str(s) = &lit.lit
770            && let Ok(ty) = syn::parse_str::<syn::Type>(&s.value())
771        {
772            return Some(ty);
773        }
774    }
775    None
776}
777
778/// Parses `#[clap_mcp_output_one_of = "T1, T2, T3"]` from enum attributes (for oneOf output schema).
779fn get_clap_mcp_output_one_of(attrs: &[syn::Attribute]) -> Option<Vec<syn::Type>> {
780    for attr in attrs {
781        if !attr.path().is_ident("clap_mcp_output_one_of") {
782            continue;
783        }
784        if let Meta::NameValue(MetaNameValue { value, .. }) = &attr.meta
785            && let Expr::Lit(lit) = value
786            && let Lit::Str(s) = &lit.lit
787        {
788            let types: Result<Vec<syn::Type>, _> = s
789                .value()
790                .split(',')
791                .map(|p| syn::parse_str::<syn::Type>(p.trim()))
792                .collect();
793            return types.ok();
794        }
795    }
796    None
797}
798
799/// Parses #[clap_mcp(requires)] or #[clap_mcp(requires = "arg_name")] from field attributes.
800/// Returns Some(arg_name) when present; empty string means use the field's own ident.
801fn get_clap_mcp_requires(attrs: &[syn::Attribute]) -> Option<String> {
802    for attr in attrs {
803        if !attr.path().is_ident("clap_mcp") {
804            continue;
805        }
806        let mut result = None;
807        let _ = attr.parse_nested_meta(|meta| {
808            if meta.path.is_ident("requires") {
809                if meta.input.peek(syn::token::Eq) {
810                    let value: Expr = meta.value()?.parse()?;
811                    if let Expr::Lit(lit) = value
812                        && let Lit::Str(s) = &lit.lit
813                    {
814                        result = Some(s.value());
815                    }
816                } else {
817                    result = Some(String::new()); // use field ident
818                }
819            }
820            Ok(())
821        });
822        if result.is_some() {
823            return result;
824        }
825    }
826    None
827}
828
829/// Returns true if the field has #[arg(long)] or #[arg(short)] (i.e. is a flag/option, not positional).
830fn field_has_arg_long_or_short(attrs: &[syn::Attribute]) -> bool {
831    for attr in attrs {
832        if !attr.path().is_ident("arg") {
833            continue;
834        }
835        let mut has = false;
836        let _ = attr.parse_nested_meta(|meta| {
837            if meta.path.is_ident("long") || meta.path.is_ident("short") {
838                has = true;
839            }
840            Ok(())
841        });
842        if has {
843            return true;
844        }
845    }
846    false
847}
848
849/// Returns true if the field has #[arg(index = ...)].
850fn field_has_arg_index(attrs: &[syn::Attribute]) -> bool {
851    for attr in attrs {
852        if !attr.path().is_ident("arg") {
853            continue;
854        }
855        let mut has = false;
856        let _ = attr.parse_nested_meta(|meta| {
857            if meta.path.is_ident("index") {
858                has = true;
859            }
860            Ok(())
861        });
862        if has {
863            return true;
864        }
865    }
866    false
867}
868
869/// Heuristic: true if the field looks like a positional (no long/short, or has index).
870fn field_looks_positional(attrs: &[syn::Attribute]) -> bool {
871    field_has_arg_index(attrs) || !field_has_arg_long_or_short(attrs)
872}
873
874/// Gets command name from #[command(name = "x")] or converts ident to kebab-case.
875fn get_command_name(attrs: &[syn::Attribute], ident: &syn::Ident) -> String {
876    for attr in attrs {
877        if !attr.path().is_ident("command") {
878            continue;
879        }
880        let mut name = None;
881        let _ = attr.parse_nested_meta(|meta| {
882            if meta.path.is_ident("name") {
883                let value: Expr = meta.value()?.parse()?;
884                if let Expr::Lit(lit) = value
885                    && let Lit::Str(s) = &lit.lit
886                {
887                    name = Some(s.value());
888                }
889            }
890            Ok(())
891        });
892        if let Some(n) = name {
893            return n;
894        }
895    }
896    ident_to_kebab(ident)
897}
898
899fn inner_type_if_option(ty: &Type) -> Option<&Type> {
900    let Type::Path(type_path) = ty else {
901        return None;
902    };
903    let last = type_path.path.segments.last()?;
904    if last.ident != "Option" {
905        return None;
906    }
907    let PathArguments::AngleBracketed(args) = &last.arguments else {
908        return None;
909    };
910    args.args.first().and_then(|a| {
911        if let GenericArgument::Type(t) = a {
912            Some(t)
913        } else {
914            None
915        }
916    })
917}
918
919fn ident_to_kebab(ident: &syn::Ident) -> String {
920    let s = ident.to_string();
921    let mut out = String::new();
922    for (i, c) in s.chars().enumerate() {
923        if c.is_uppercase() && i > 0 {
924            out.push('-');
925        }
926        for c in c.to_lowercase() {
927            out.push(c);
928        }
929    }
930    out
931}
932
933/// Returns true if the type is `Option<T>`.
934fn is_option_type(ty: &Type) -> bool {
935    let Type::Path(type_path) = ty else {
936        return false;
937    };
938    let Some(last) = type_path.path.segments.last() else {
939        return false;
940    };
941    if last.ident != "Option" {
942        return false;
943    }
944    let PathArguments::AngleBracketed(args) = &last.arguments else {
945        return false;
946    };
947    let type_args: Vec<_> = args
948        .args
949        .iter()
950        .filter_map(|a| {
951            if let GenericArgument::Type(_) = a {
952                Some(())
953            } else {
954                None
955            }
956        })
957        .collect();
958    type_args.len() == 1
959}
960
961fn field_is_repeated_mcp_scalar(ty: &Type) -> bool {
962    is_positional_scalar_field(ty)
963}
964
965fn is_positional_scalar_field(ty: &Type) -> bool {
966    let ty = inner_type_if_option(ty).unwrap_or(ty);
967    let Type::Path(type_path) = ty else {
968        return true;
969    };
970    let Some(last) = type_path.path.segments.last() else {
971        return true;
972    };
973    if last.ident == "Vec" {
974        return false;
975    }
976    true
977}
978
979fn has_ambiguous_mcp_positionals<'a, I>(fields: I) -> bool
980where
981    I: IntoIterator<Item = &'a syn::Field>,
982{
983    let mut positional_scalars = 0usize;
984    for field in fields {
985        if has_clap_mcp_skip(&field.attrs)
986            || !field_looks_positional(&field.attrs)
987            || !field_is_repeated_mcp_scalar(&field.ty)
988        {
989            continue;
990        }
991        positional_scalars += 1;
992        if positional_scalars > 1 {
993            return true;
994        }
995    }
996    false
997}
998
999fn strip_option_type(ty: &Type) -> Type {
1000    inner_type_if_option(ty)
1001        .map(|t| (*t).clone())
1002        .unwrap_or_else(|| ty.clone())
1003}
1004
1005fn subcommand_field_type_from_enum(data: &syn::DataEnum) -> Option<Type> {
1006    let mut found: Option<Type> = None;
1007    for variant in &data.variants {
1008        for field in variant.fields.iter() {
1009            if !field_has_command_subcommand(&field.attrs) {
1010                continue;
1011            }
1012            let ty = strip_option_type(&field.ty);
1013            if let Some(prev) = &found {
1014                if prev.to_token_stream().to_string() != ty.to_token_stream().to_string() {
1015                    return None;
1016                }
1017            } else {
1018                found = Some(ty);
1019            }
1020        }
1021    }
1022    found
1023}
1024
1025fn nested_subcommand_type_paths_from_enum(data: &syn::DataEnum) -> Vec<syn::Path> {
1026    let mut paths = Vec::new();
1027    let mut seen = std::collections::HashSet::new();
1028    for variant in &data.variants {
1029        for field in variant.fields.iter() {
1030            if !field_has_command_subcommand(&field.attrs) {
1031                continue;
1032            }
1033            let sub_ty = inner_type_if_option(&field.ty).unwrap_or(&field.ty);
1034            if let syn::Type::Path(tp) = sub_ty {
1035                let key = tp.path.to_token_stream().to_string();
1036                if seen.insert(key) {
1037                    paths.push(tp.path.clone());
1038                }
1039            }
1040        }
1041    }
1042    paths
1043}
1044
1045/// Derive macro for `ClapMcpConfigProvider` and `ClapMcpToolExecutor`.
1046///
1047/// Use on a clap `Parser` enum to expose it over MCP. Implements execution safety
1048/// config and tool output generation.
1049///
1050/// ## Struct root with subcommand
1051///
1052/// When your CLI has a **struct root** with `#[command(subcommand)]`, derive
1053/// `ClapMcp` on **both** the root struct and the subcommand enum. Put
1054/// `#[clap_mcp(...)]` on the **root struct** (the type that implements
1055/// `parse_or_serve_mcp()`); put `#[clap_mcp_output_from = "run"]` on the
1056/// **subcommand enum**. The root delegates tool execution to the subcommand.
1057///
1058/// # Attributes
1059///
1060/// ## `#[clap_mcp(...)]` (on the enum)
1061///
1062/// - `parallel_safe` / `parallel_safe = true|false` — If true, tool calls may run concurrently.
1063/// - `reinvocation_safe` / `reinvocation_safe = true|false` — If true, uses in-process execution.
1064/// - `share_runtime` / `share_runtime = true|false` — When reinvocation_safe, whether async tools
1065///   (via `clap_mcp::run_async_tool`) share the MCP server's tokio runtime (`true`) or use a
1066///   dedicated thread (`false`, default). Ignored when reinvocation_safe is false.
1067/// - `catch_in_process_panics` / `catch_in_process_panics = true|false` — When reinvocation_safe,
1068///   if true, panics in tool code are caught and returned as MCP errors instead of crashing the
1069///   server. Default is false. See [`ClapMcpConfig::catch_in_process_panics`].
1070/// - `allow_mcp_without_subcommand` / `allow_mcp_without_subcommand = true|false` — When true
1071///   (default), `myapp --mcp` starts MCP even when the root has `subcommand_required = true`
1072///   (argv is checked before clap). Does **not** change non-MCP CLI behavior; do not switch to
1073///   `Option<Commands>` solely for MCP. See [`ClapMcpConfig::allow_mcp_without_subcommand`].
1074/// - `mcp_flag = "long_name"` — Rename the stdio MCP flag long name (default `"mcp"`). clap arg
1075///   id stays [`CLAP_MCP_STDIO_FLAG_ID`](clap_mcp::CLAP_MCP_STDIO_FLAG_ID).
1076/// - `mcp_http_flag = "long_name"` — Rename the HTTP MCP flag (requires `http` feature).
1077/// - `export_skills_flag = "long_name"` — Rename the export-skills flag.
1078/// - `task_augmented_tools` / `task_augmented_tools = true|false` — When true, advertise MCP task
1079///   support and handle task-augmented `tools/call` (in-process only). Requires
1080///   `reinvocation_safe`; combining with `reinvocation_safe = false` is a **compile error**.
1081///   With `parallel_safe = false`, task and plain tool bodies share one serialization queue.
1082///   With `parallel_safe = true`, task bodies may overlap with each other and with plain
1083///   `tools/call`; logging during tasks uses per-task context so `meta.taskId` stays correct.
1084///   `catch_in_process_panics = true` maps panics in task-scheduled work to task error payloads.
1085/// - `stateful` / `stateful = true|false` — On a struct root (or delegating enum) with a
1086///   subcommand field, implement [`ClapMcpToolExecutorWithState`] by delegating to the
1087///   subcommand. Requires `reinvocation_safe`. Not for multi-user or untrusted remote MCP
1088///   callers; see [`ClapMcpToolExecutorWithState`].
1089///
1090/// ## `#[clap_mcp(task)]` (on variant)
1091///
1092/// When `task_augmented_tools` is enabled, marks this subcommand as eligible for task-augmented
1093/// `tools/call`. If **no** variant has `#[clap_mcp(task)]`, **all** tools are eligible.
1094///
1095/// ## `#[clap_mcp_output_from = "run"]` (on the enum)
1096///
1097/// When present, tool execution is driven by a single function instead of per-variant attributes.
1098/// The value is the path to a function (e.g. `"run"` or `"my_mod::run"`) that takes the CLI type
1099/// by value and returns a type implementing `IntoClapMcpResult` (e.g. `String`, `AsStructured<T>`,
1100/// `Option<O>`, `Result<O, E>`). The macro generates `execute_for_mcp(self)` as
1101/// `run(self).into_tool_result()`. **Required** for enums.
1102///
1103/// ## `#[clap_mcp_output_type = "TypeName"]` (on the enum, requires `output-schema` feature)
1104///
1105/// When present and the crate is built with `output-schema`, the type's JSON schema (via
1106/// `schemars::JsonSchema`) is set on [`ClapMcpSchemaMetadata::output_schema`] so each tool
1107/// gets an `output_schema` for MCP clients.
1108///
1109/// ## `#[clap_mcp_output_one_of = "T1, T2, T3"]` (on the enum, requires `output-schema` feature)
1110///
1111/// When present and the crate is built with `output-schema`, builds a JSON schema with `oneOf`
1112/// from the listed types (each must implement `schemars::JsonSchema`) and sets it on
1113/// [`ClapMcpSchemaMetadata::output_schema`]. Use when you want an explicit list of output
1114/// types without a wrapper enum. If both `output_type` and `output_one_of` are set,
1115/// `output_one_of` is used.
1116///
1117/// ## Stateful tools (`#[clap_mcp_output_from_with_state]`, `#[clap_mcp(stateful)]`)
1118///
1119/// For session state across MCP tool calls (requires `reinvocation_safe`), see
1120/// [`ClapMcpToolExecutorWithState`]. Session state is shared for the MCP server process
1121/// lifetime, not per MCP client; see [Stateful MCP tools](https://github.com/canardleteer/clap-mcp/blob/main/docs/stateful-tools.md)
1122/// and [Security](https://github.com/canardleteer/clap-mcp/blob/main/docs/security.md).
1123/// On the **leaf** subcommand enum:
1124///
1125/// - `#[clap_mcp_output_from_with_state = "run"]` — path to `run(cmd, state: &State) -> T`
1126/// - `#[clap_mcp_state_type = "Type"]` — must match the second parameter of `run` (without `&`)
1127///
1128/// On struct roots or intermediate subcommand enums that delegate to a stateful subcommand:
1129///
1130/// - `#[clap_mcp(stateful)]` — implements `ClapMcpToolExecutorWithState` with
1131///   `type State = <Subcommand as ClapMcpToolExecutorWithState>::State` (no duplicate
1132///   `state_type`).
1133///
1134/// ## Positional arguments and MCP
1135///
1136/// MCP clients send **named** JSON; clap-mcp rebuilds argv for tool execution. Two or more
1137/// bare positional scalar fields on the same variant (non-`Vec`) are a **compile error** —
1138/// use `#[arg(long)]` on each field or `#[clap_mcp(skip)]`. See [PR #12](https://github.com/canardleteer/clap-mcp/pull/12).
1139///
1140/// **Trailing / passthrough args:** For cargo-style trailing argv, use
1141/// `#[arg(last = true, allow_hyphen_values = true)] command: Vec<String>` on direct CLI;
1142/// MCP clients pass `command` as a JSON array. `build_tool_argv` inserts `--` before
1143/// trailing multi-value positionals when rebuilding argv. An explicit
1144/// `#[arg(long)] args: Vec<String>` is often clearer for MCP. Pre-clap MCP detection
1145/// ignores tokens after the shell's first `--`.
1146///
1147/// ## `#[clap_mcp(skip)]` / `#[clap_mcp(skip = "id1,id2")]` (on variant or field)
1148///
1149/// Exclude the subcommand or argument from MCP exposure. On a normal field, bare
1150/// `skip` uses the field's clap arg id (field ident by default; `#[arg(id = "...")]` when set).
1151/// On a `#[command(flatten)]` `Args` field, bare `skip` probes `Args::augment_args` and
1152/// excludes every arg id from the flattened type. On a `#[command(subcommand)]` field, bare
1153/// `skip` probes `Subcommand::augment_subcommands` and adds subcommand **names** to
1154/// `skip_commands` (recursive). `skip = "id1,id2"` on flatten lists arg ids; on subcommand
1155/// lists subcommand names. External flattened types are a compile error; use imperative
1156/// `skip_commands` / `skip_args`. See [Supported CLI shapes](https://github.com/canardleteer/clap-mcp/blob/main/docs/supported-cli-shapes.md).
1157///
1158/// ## `#[clap_mcp(skip_root_when_subcommands)]` (on root struct with subcommand)
1159///
1160/// When present on a struct root that has `#[command(subcommand)]`, the root command
1161/// is excluded from the MCP tool list; only subcommands appear as tools. Equivalent to
1162/// setting `ClapMcpSchemaMetadata::skip_root_command_when_subcommands = true` imperatively.
1163///
1164/// ## `#[clap_mcp(requires)]` / `#[clap_mcp(requires = "arg_name")]` (on field)
1165///
1166/// Make the argument required in the MCP tool schema even if optional in clap.
1167/// Use `requires` for the field's own id, or `requires = "name"` to specify.
1168///
1169/// ## `#[clap_mcp(serialized)]` / `#[clap_mcp(serialized = "arg1, arg2")]` (on variant)
1170///
1171/// When [`ClapMcpConfig::parallel_safe`] is true, serializes concurrent MCP invocations of this
1172/// tool. Shorthand `serialized` locks the whole tool; `serialized = "output"` locks by arg id
1173/// (comma-separated for multiple ids). See the execution-safety guide for documented use.
1174///
1175/// Optional: `#[clap_mcp(serialize_topic)]` on a field listed in `serialized = "..."` uses
1176/// [`ClapMcpSerializeTopic`] for that arg when you opt into typed topic keys. The attribute may
1177/// also appear on fields inside shared `Args` helpers when flattened on a variant: add
1178/// `#[clap_mcp(args_metadata)]` on the `Args` struct (same crate). External `Args` types need
1179/// imperative [`ClapMcpSchemaMetadata::serialize_topic_args`]. Derive metadata keys match
1180/// clap arg ids (field ident by default; `#[arg(id = "...")]` when set). Topical locks do not isolate session
1181/// state. See [execution-safety](https://github.com/canardleteer/clap-mcp/blob/main/docs/execution-safety.md).
1182///
1183/// ## `#[clap_mcp(requires = "arg1,arg2")]` (on variant)
1184///
1185/// Variant-level alternative: one or more optional args to make required (single name or
1186/// comma-separated list). The MCP tool schema will mark each listed argument as required.
1187/// Prefer this when declaring multiple required args. When the client omits a required
1188/// arg, a clear error is returned.
1189///
1190/// # Example (idiomatic: single `run` function, no duplicated logic)
1191///
1192/// ```rust,ignore
1193/// use clap::Parser;
1194///
1195/// #[derive(Debug, Parser, clap_mcp::ClapMcp)]
1196/// #[clap_mcp(reinvocation_safe, parallel_safe = false)]
1197/// #[clap_mcp_output_from = "run"]
1198/// enum Cli {
1199///     Greet { #[arg(long)] name: Option<String> },
1200///     Add { #[arg(long)] a: i32, #[arg(long)] b: i32 },
1201/// }
1202///
1203/// fn run(cmd: Cli) -> String {
1204///     match cmd {
1205///         Cli::Greet { name } => format!("Hello, {}!", name.as_deref().unwrap_or("world")),
1206///         Cli::Add { a, b } => (a + b).to_string(),
1207///     }
1208/// }
1209/// ```
1210#[proc_macro_derive(
1211    ClapMcp,
1212    attributes(
1213        clap_mcp,
1214        clap_mcp_output_from,
1215        clap_mcp_output_from_with_state,
1216        clap_mcp_state_type,
1217        clap_mcp_output_type,
1218        clap_mcp_output_one_of,
1219        command,
1220        arg
1221    )
1222)]
1223pub fn derive_clap_mcp(input: TokenStream) -> TokenStream {
1224    let input = parse_macro_input!(input as DeriveInput);
1225
1226    if has_clap_mcp_args_metadata(&input.attrs) {
1227        return TokenStream::from(build_args_metadata_impl(&input));
1228    }
1229
1230    let mut serialized_flatten_const_checks: Vec<proc_macro2::TokenStream> = Vec::new();
1231
1232    match &input.data {
1233        syn::Data::Enum(data) => {
1234            for variant in &data.variants {
1235                if has_clap_mcp_skip(&variant.attrs) {
1236                    continue;
1237                }
1238                if has_ambiguous_mcp_positionals(variant.fields.iter()) {
1239                    return TokenStream::from(
1240                        syn::Error::new_spanned(
1241                            &variant.ident,
1242                            "clap_mcp: multiple positional scalar arguments are ambiguous for MCP \
1243                             tool calls; use #[arg(long)] on each field or #[clap_mcp(skip)]",
1244                        )
1245                        .to_compile_error(),
1246                    );
1247                }
1248                if let Some(serialized) = get_clap_mcp_serialized(&variant.attrs) {
1249                    let field_ids: std::collections::HashSet<String> =
1250                        variant_field_ids(&variant.fields).into_iter().collect();
1251                    let flatten_types: Vec<syn::Type> = variant
1252                        .fields
1253                        .iter()
1254                        .filter(|f| field_has_command_flatten(&f.attrs))
1255                        .map(|f| inner_type_if_option(&f.ty).unwrap_or(&f.ty).clone())
1256                        .collect();
1257                    if let ClapMcpSerialized::Args(arg_ids) = &serialized {
1258                        for arg_id in arg_ids {
1259                            if field_ids.contains(arg_id) {
1260                                continue;
1261                            }
1262                            if flatten_types.is_empty() {
1263                                return TokenStream::from(
1264                                    syn::Error::new_spanned(
1265                                        &variant.ident,
1266                                        format!(
1267                                            "clap_mcp: serialized = \"{arg_id}\" — no field or arg \
1268                                             with id `{arg_id}` on this variant"
1269                                        ),
1270                                    )
1271                                    .to_compile_error(),
1272                                );
1273                            }
1274                            let arg_lit = syn::LitStr::new(arg_id, proc_macro2::Span::call_site());
1275                            let checks = flatten_types.iter().map(|ty| {
1276                                quote! { clap_mcp::flatten_args_contains_field::<#ty>(#arg_lit) }
1277                            });
1278                            serialized_flatten_const_checks.push(quote! {
1279                                const _: () = {
1280                                    clap_mcp::assert_serialized_in_any_flatten_args(
1281                                        #arg_lit,
1282                                        &[#(#checks),*],
1283                                    );
1284                                };
1285                            });
1286                        }
1287                    }
1288                    for (i, f) in variant.fields.iter().enumerate() {
1289                        if !has_clap_mcp_serialize_topic(&f.attrs) {
1290                            continue;
1291                        }
1292                        let arg_id = f
1293                            .ident
1294                            .as_ref()
1295                            .map(|ident| clap_arg_id_from_field(ident, &f.attrs))
1296                            .unwrap_or_else(|| format!("__f{i}"));
1297                        let Some(scope_args) = serialized_scope_arg_ids(&serialized) else {
1298                            return TokenStream::from(
1299                                syn::Error::new_spanned(
1300                                    f.ident.as_ref().unwrap_or(&variant.ident),
1301                                    "clap_mcp: #[clap_mcp(serialize_topic)] requires arg-scoped \
1302                                     #[clap_mcp(serialized = \"arg_id\")] on the same variant",
1303                                )
1304                                .to_compile_error(),
1305                            );
1306                        };
1307                        if !scope_args.contains(&arg_id) {
1308                            return TokenStream::from(
1309                                syn::Error::new_spanned(
1310                                    f.ident.as_ref().unwrap_or(&variant.ident),
1311                                    format!(
1312                                        "clap_mcp: #[clap_mcp(serialize_topic)] on `{arg_id}` \
1313                                         requires that arg in #[clap_mcp(serialized = \"...\")]"
1314                                    ),
1315                                )
1316                                .to_compile_error(),
1317                            );
1318                        }
1319                    }
1320                }
1321            }
1322        }
1323        syn::Data::Struct(data) => {
1324            let subcommand_field = data
1325                .fields
1326                .iter()
1327                .find(|f| field_has_command_subcommand(&f.attrs));
1328            if has_ambiguous_mcp_positionals(
1329                data.fields
1330                    .iter()
1331                    .filter(|f| !subcommand_field.is_some_and(|sf| std::ptr::eq(sf, *f))),
1332            ) {
1333                return TokenStream::from(
1334                    syn::Error::new_spanned(
1335                        &input.ident,
1336                        "clap_mcp: multiple positional scalar arguments are ambiguous for MCP \
1337                         tool calls; use #[arg(long)] on each field or #[clap_mcp(skip)]",
1338                    )
1339                    .to_compile_error(),
1340                );
1341            }
1342        }
1343        _ => {}
1344    }
1345
1346    let output_from_with_state = get_clap_mcp_output_from_with_state(&input.attrs);
1347    let state_type = get_clap_mcp_state_type(&input.attrs);
1348    let schema_only = has_clap_mcp_schema_only(&input.attrs);
1349    if schema_only {
1350        if !matches!(&input.data, syn::Data::Enum(_)) {
1351            return TokenStream::from(
1352                syn::Error::new_spanned(
1353                    &input.ident,
1354                    "clap_mcp: #[clap_mcp(schema_only)] is only supported on subcommand enums",
1355                )
1356                .to_compile_error(),
1357            );
1358        }
1359        if get_clap_mcp_output_from(&input.attrs).is_some()
1360            || output_from_with_state.is_some()
1361            || get_clap_mcp_state_type(&input.attrs).is_some()
1362        {
1363            return TokenStream::from(
1364                syn::Error::new_spanned(
1365                    &input.ident,
1366                    "clap_mcp: #[clap_mcp(schema_only)] cannot be combined with \
1367                     #[clap_mcp_output_from], #[clap_mcp_output_from_with_state], or \
1368                     #[clap_mcp_state_type]",
1369                )
1370                .to_compile_error(),
1371            );
1372        }
1373    }
1374    if state_type.is_some() && output_from_with_state.is_none() {
1375        return TokenStream::from(
1376            syn::Error::new_spanned(
1377                &input.ident,
1378                "clap_mcp: #[clap_mcp_state_type = \"Type\"] requires \
1379                 #[clap_mcp_output_from_with_state = \"run\"] on the same type",
1380            )
1381            .to_compile_error(),
1382        );
1383    }
1384
1385    let name = &input.ident;
1386    let (
1387        parallel_safe,
1388        reinvocation_safe,
1389        share_runtime,
1390        catch_in_process_panics,
1391        allow_mcp_without_subcommand,
1392        task_augmented_tools,
1393        stateful,
1394        mcp_flag,
1395        mcp_http_flag,
1396        export_skills_flag,
1397    ) = parse_clap_mcp_attrs(&input.attrs);
1398    let stateful_effective = stateful.unwrap_or(false);
1399
1400    let reinvocation_effective = reinvocation_safe.unwrap_or(false);
1401    if task_augmented_tools == Some(true) && !reinvocation_effective {
1402        return TokenStream::from(
1403            syn::Error::new(
1404                proc_macro2::Span::call_site(),
1405                "clap_mcp: task_augmented_tools requires reinvocation_safe (in-process execution); subprocess mode cannot use MCP task-augmented tools/call",
1406            )
1407            .to_compile_error(),
1408        );
1409    }
1410    if output_from_with_state.is_some() && !reinvocation_effective {
1411        return TokenStream::from(
1412            syn::Error::new_spanned(
1413                &input.ident,
1414                "clap_mcp: stateful MCP tools require reinvocation_safe (in-process execution)",
1415            )
1416            .to_compile_error(),
1417        );
1418    }
1419    if stateful_effective && !reinvocation_effective {
1420        return TokenStream::from(
1421            syn::Error::new_spanned(
1422                &input.ident,
1423                "clap_mcp: #[clap_mcp(stateful)] requires reinvocation_safe (in-process execution)",
1424            )
1425            .to_compile_error(),
1426        );
1427    }
1428    if schema_only && stateful_effective {
1429        return TokenStream::from(
1430            syn::Error::new_spanned(
1431                &input.ident,
1432                "clap_mcp: #[clap_mcp(schema_only)] cannot be combined with #[clap_mcp(stateful)]",
1433            )
1434            .to_compile_error(),
1435        );
1436    }
1437
1438    let parallel_safe_expr = parallel_safe
1439        .map(|b| quote! { #b })
1440        .unwrap_or_else(|| quote! { clap_mcp::ClapMcpConfig::default().parallel_safe });
1441    let reinvocation_safe_expr = reinvocation_safe
1442        .map(|b| quote! { #b })
1443        .unwrap_or_else(|| quote! { clap_mcp::ClapMcpConfig::default().reinvocation_safe });
1444    let share_runtime_expr = share_runtime
1445        .map(|b| quote! { #b })
1446        .unwrap_or_else(|| quote! { clap_mcp::ClapMcpConfig::default().share_runtime });
1447    let catch_in_process_panics_expr = catch_in_process_panics
1448        .map(|b| quote! { #b })
1449        .unwrap_or_else(|| quote! { clap_mcp::ClapMcpConfig::default().catch_in_process_panics });
1450    let allow_mcp_without_subcommand_expr = allow_mcp_without_subcommand
1451        .map(|b| quote! { #b })
1452        .unwrap_or_else(
1453            || quote! { clap_mcp::ClapMcpConfig::default().allow_mcp_without_subcommand },
1454        );
1455
1456    let mut builtin_flag_stmts = Vec::new();
1457    if let Some(long) = mcp_flag {
1458        builtin_flag_stmts.push(quote! { flags = flags.with_stdio_long(#long); });
1459    }
1460    if let Some(long) = mcp_http_flag {
1461        builtin_flag_stmts.push(quote! { flags = flags.with_http_long(#long); });
1462    }
1463    if let Some(long) = export_skills_flag {
1464        builtin_flag_stmts.push(quote! { flags = flags.with_export_skills_long(#long); });
1465    }
1466    let builtin_flags_impl = if builtin_flag_stmts.is_empty() {
1467        quote! { clap_mcp::ClapMcpBuiltinFlags::default() }
1468    } else {
1469        quote! {{
1470            let mut flags = clap_mcp::ClapMcpBuiltinFlags::default();
1471            #(#builtin_flag_stmts)*
1472            flags
1473        }}
1474    };
1475
1476    let config_provider = quote! {
1477        impl clap_mcp::ClapMcpConfigProvider for #name {
1478            fn clap_mcp_config() -> clap_mcp::ClapMcpConfig {
1479                clap_mcp::ClapMcpConfig {
1480                    parallel_safe: #parallel_safe_expr,
1481                    reinvocation_safe: #reinvocation_safe_expr,
1482                    share_runtime: #share_runtime_expr,
1483                    catch_in_process_panics: #catch_in_process_panics_expr,
1484                    allow_mcp_without_subcommand: #allow_mcp_without_subcommand_expr,
1485                    builtin_flags: #builtin_flags_impl,
1486                }
1487            }
1488        }
1489    };
1490
1491    let executor_impl = match &input.data {
1492        syn::Data::Enum(data) => {
1493            if schema_only {
1494                quote! {}
1495            } else {
1496                let run_path = get_clap_mcp_output_from(&input.attrs);
1497                let run_with_state = output_from_with_state.as_ref();
1498                let state_ty = state_type.as_ref();
1499                let projected_sub = subcommand_field_type_from_enum(data);
1500                match (run_path, run_with_state, state_ty) {
1501                    (Some(run), None, None) => quote! {
1502                        impl clap_mcp::ClapMcpToolExecutor for #name {
1503                            fn execute_for_mcp(self) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
1504                                clap_mcp::IntoClapMcpResult::into_tool_result(#run(self))
1505                            }
1506                        }
1507                    },
1508                    (None, Some(run), Some(st)) => quote! {
1509                        impl clap_mcp::ClapMcpToolExecutorWithState for #name {
1510                            type State = #st;
1511                            fn execute_for_mcp_with_state(
1512                                self,
1513                                state: &Self::State,
1514                            ) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
1515                                clap_mcp::IntoClapMcpResult::into_tool_result(#run(self, state))
1516                            }
1517                        }
1518                    },
1519                    (None, Some(run), None) => {
1520                        if let Some(sub_ty) = projected_sub {
1521                            quote! {
1522                                impl clap_mcp::ClapMcpToolExecutorWithState for #name {
1523                                    type State = <#sub_ty as clap_mcp::ClapMcpToolExecutorWithState>::State;
1524                                    fn execute_for_mcp_with_state(
1525                                        self,
1526                                        state: &Self::State,
1527                                    ) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
1528                                        clap_mcp::IntoClapMcpResult::into_tool_result(#run(self, state))
1529                                    }
1530                                }
1531                            }
1532                        } else {
1533                            let err = syn::Error::new_spanned(
1534                                &input.ident,
1535                                "clap_mcp: #[clap_mcp_output_from_with_state = \"run\"] on a leaf enum \
1536                             requires #[clap_mcp_state_type = \"Type\"] matching the second \
1537                             parameter of run (e.g. run(cmd, state: &Mutex<S>) → \
1538                             #[clap_mcp_state_type = \"Mutex<S>\"])",
1539                            );
1540                            return TokenStream::from(err.to_compile_error());
1541                        }
1542                    }
1543                    (Some(run), Some(run_st), Some(st)) => quote! {
1544                        impl clap_mcp::ClapMcpToolExecutor for #name {
1545                            fn execute_for_mcp(self) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
1546                                clap_mcp::IntoClapMcpResult::into_tool_result(#run(self))
1547                            }
1548                        }
1549                        impl clap_mcp::ClapMcpToolExecutorWithState for #name {
1550                            type State = #st;
1551                            fn execute_for_mcp_with_state(
1552                                self,
1553                                state: &Self::State,
1554                            ) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
1555                                clap_mcp::IntoClapMcpResult::into_tool_result(#run_st(self, state))
1556                            }
1557                        }
1558                    },
1559                    _ => {
1560                        let err = syn::Error::new_spanned(
1561                            &input.ident,
1562                            "clap_mcp: enum must have #[clap_mcp_output_from = \"run\"] and/or \
1563                         #[clap_mcp_output_from_with_state = \"run\"] (with \
1564                         #[clap_mcp_state_type = \"Type\"] on leaf enums), or \
1565                         #[clap_mcp(schema_only)] when an ancestor owns tool execution",
1566                        );
1567                        return TokenStream::from(err.to_compile_error());
1568                    }
1569                }
1570            }
1571        }
1572        syn::Data::Struct(data) => {
1573            let struct_run_path = get_clap_mcp_output_from(&input.attrs);
1574            let subcommand_field = data
1575                .fields
1576                .iter()
1577                .find(|f| field_has_command_subcommand(&f.attrs));
1578            match subcommand_field {
1579                Some(field) => {
1580                    if let Some(run) = struct_run_path {
1581                        if stateful_effective || output_from_with_state.is_some() {
1582                            let err = syn::Error::new_spanned(
1583                                &input.ident,
1584                                "clap_mcp: #[clap_mcp_output_from] on a struct root cannot be \
1585                                 combined with #[clap_mcp(stateful)] or \
1586                                 #[clap_mcp_output_from_with_state]; use subcommand delegation \
1587                                 or a manual ClapMcpToolExecutor instead",
1588                            );
1589                            return TokenStream::from(err.to_compile_error());
1590                        }
1591                        quote! {
1592                            impl clap_mcp::ClapMcpToolExecutor for #name {
1593                                fn execute_for_mcp(self) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
1594                                    clap_mcp::IntoClapMcpResult::into_tool_result(#run(self))
1595                                }
1596                            }
1597                        }
1598                    } else {
1599                        let field_ident = match &field.ident {
1600                            Some(id) => id.clone(),
1601                            None => {
1602                                let err = syn::Error::new_spanned(
1603                                    field,
1604                                    "clap_mcp: subcommand field must be named",
1605                                );
1606                                return TokenStream::from(err.to_compile_error());
1607                            }
1608                        };
1609                        let body = if is_option_type(&field.ty) {
1610                            quote! {
1611                                self.#field_ident.map_or_else(
1612                                    || Ok(clap_mcp::ClapMcpToolOutput::Text(String::new())),
1613                                    |c| c.execute_for_mcp(),
1614                                )
1615                            }
1616                        } else {
1617                            quote! {
1618                                self.#field_ident.execute_for_mcp()
1619                            }
1620                        };
1621                        let state_body = if is_option_type(&field.ty) {
1622                            quote! {
1623                                self.#field_ident.map_or_else(
1624                                    || Ok(clap_mcp::ClapMcpToolOutput::Text(String::new())),
1625                                    |c| c.execute_for_mcp_with_state(state),
1626                                )
1627                            }
1628                        } else {
1629                            quote! {
1630                                self.#field_ident.execute_for_mcp_with_state(state)
1631                            }
1632                        };
1633                        let mut impls = proc_macro2::TokenStream::new();
1634                        if !stateful_effective {
1635                            impls.extend(quote! {
1636                            impl clap_mcp::ClapMcpToolExecutor for #name {
1637                                fn execute_for_mcp(self) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
1638                                    #body
1639                                }
1640                            }
1641                        });
1642                        } else {
1643                            let sub_ty = strip_option_type(&field.ty);
1644                            impls.extend(quote! {
1645                            impl clap_mcp::ClapMcpToolExecutorWithState for #name {
1646                                type State = <#sub_ty as clap_mcp::ClapMcpToolExecutorWithState>::State;
1647                                fn execute_for_mcp_with_state(
1648                                    self,
1649                                    state: &Self::State,
1650                                ) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
1651                                    #state_body
1652                                }
1653                            }
1654                        });
1655                        }
1656                        impls
1657                    }
1658                }
1659                None => {
1660                    if output_from_with_state.is_some() || stateful_effective {
1661                        let err = syn::Error::new_spanned(
1662                            &input.ident,
1663                            "clap_mcp: #[clap_mcp_output_from_with_state] and #[clap_mcp(stateful)] \
1664                             require a named subcommand field",
1665                        );
1666                        return TokenStream::from(err.to_compile_error());
1667                    }
1668                    if let Some(run) = struct_run_path {
1669                        quote! {
1670                            impl clap_mcp::ClapMcpToolExecutor for #name {
1671                                fn execute_for_mcp(self) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
1672                                    clap_mcp::IntoClapMcpResult::into_tool_result(#run(self))
1673                                }
1674                            }
1675                        }
1676                    } else {
1677                        quote! {
1678                            impl clap_mcp::ClapMcpToolExecutor for #name {
1679                                fn execute_for_mcp(self) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
1680                                    Ok(clap_mcp::ClapMcpToolOutput::Text(format!("{:?}", self)))
1681                                }
1682                            }
1683                        }
1684                    }
1685                }
1686            }
1687        }
1688        _ => quote! {
1689            impl clap_mcp::ClapMcpToolExecutor for #name {
1690                fn execute_for_mcp(self) -> std::result::Result<clap_mcp::ClapMcpToolOutput, clap_mcp::ClapMcpToolError> {
1691                    Ok(clap_mcp::ClapMcpToolOutput::Text(format!("{:?}", self)))
1692                }
1693            }
1694        },
1695    };
1696
1697    let schema_metadata_impl = build_schema_metadata_impl(&input);
1698
1699    let expanded = quote! {
1700        #(#serialized_flatten_const_checks)*
1701        #config_provider
1702        #executor_impl
1703        #schema_metadata_impl
1704    };
1705
1706    TokenStream::from(expanded)
1707}
1708
1709/// Builds the ClapMcpSchemaMetadataProvider impl from #[clap_mcp(skip)], #[clap_mcp(requires)], and #[clap_mcp(task)].
1710fn serialize_topic_bindings_quote(
1711    target: &syn::Ident,
1712    bindings: &[(String, String, syn::Type)],
1713) -> proc_macro2::TokenStream {
1714    let entries = bindings.iter().map(|(cmd, arg, ty)| {
1715        let cmd_lit = syn::LitStr::new(cmd, proc_macro2::Span::call_site());
1716        let arg_lit = syn::LitStr::new(arg, proc_macro2::Span::call_site());
1717        quote! {
1718            #target.serialize_topic_args
1719                .entry(#cmd_lit.to_string())
1720                .or_default()
1721                .insert(
1722                    #arg_lit.to_string(),
1723                    <#ty as clap_mcp::ClapMcpSerializeTopic>::serialize_topic_segment,
1724                );
1725        }
1726    });
1727    quote! { #(#entries)* }
1728}
1729
1730/// Builds the ClapMcpSchemaMetadataProvider impl from #[clap_mcp(skip)], #[clap_mcp(requires)], and #[clap_mcp(task)].
1731fn build_schema_metadata_impl(input: &DeriveInput) -> proc_macro2::TokenStream {
1732    let name = &input.ident;
1733    let (_, _, _, _, _, task_augmented_tools, _, _, _, _) = parse_clap_mcp_attrs(&input.attrs);
1734    let task_augmented_tools_expr = task_augmented_tools
1735        .map(|b| quote! { #b })
1736        .unwrap_or(quote! { false });
1737    let mut skip_commands = Vec::<String>::new();
1738    let mut skip_args: std::collections::HashMap<String, Vec<String>> =
1739        std::collections::HashMap::new();
1740    let mut requires_args: std::collections::HashMap<String, Vec<String>> =
1741        std::collections::HashMap::new();
1742    let mut task_tool_names = Vec::<String>::new();
1743    let mut serialize_tools: std::collections::HashMap<String, ClapMcpSerialized> =
1744        std::collections::HashMap::new();
1745    let mut serialize_topic_bindings: Vec<(String, String, syn::Type)> = Vec::new();
1746    let mut flatten_serialize_topic_cmds: Vec<(String, syn::Type)> = Vec::new();
1747    let mut flatten_skip_entries: Vec<FlattenSkipEntry> = Vec::new();
1748    let mut flatten_skip_error: Option<syn::Error> = None;
1749    let mut warn_optional_positional = false;
1750
1751    let optional_positional_warn_block: proc_macro2::TokenStream = quote! {
1752        #[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)")]
1753        fn _clap_mcp_optional_positional_warn() {}
1754        _clap_mcp_optional_positional_warn();
1755    };
1756
1757    let output_schema_assign: proc_macro2::TokenStream =
1758        if let Some(types) = get_clap_mcp_output_one_of(&input.attrs) {
1759            if types.is_empty() {
1760                quote! {}
1761            } else {
1762                quote! { m.output_schema = clap_mcp::output_schema_one_of!(#(#types),*); }
1763            }
1764        } else if let Some(ty) = get_clap_mcp_output_type(&input.attrs) {
1765            quote! { m.output_schema = clap_mcp::output_schema_for_type::<#ty>(); }
1766        } else {
1767            quote! {}
1768        };
1769
1770    match &input.data {
1771        syn::Data::Enum(data) => {
1772            for v in &data.variants {
1773                let cmd_name = get_command_name(&v.attrs, &v.ident);
1774                let variant_reqs = get_clap_mcp_requires_variant(&v.attrs).unwrap_or_default();
1775                if has_clap_mcp_skip(&v.attrs) {
1776                    skip_commands.push(cmd_name.clone());
1777                }
1778                if has_clap_mcp_task(&v.attrs) {
1779                    task_tool_names.push(cmd_name.clone());
1780                }
1781                let variant_has_serialized_args = matches!(
1782                    get_clap_mcp_serialized(&v.attrs),
1783                    Some(ClapMcpSerialized::Args(_))
1784                );
1785                if let Some(serialized) = get_clap_mcp_serialized(&v.attrs) {
1786                    serialize_tools.insert(cmd_name.clone(), serialized);
1787                }
1788                requires_args
1789                    .entry(cmd_name.clone())
1790                    .or_default()
1791                    .extend(variant_reqs.clone());
1792                for (i, f) in v.fields.iter().enumerate() {
1793                    let arg_id = f
1794                        .ident
1795                        .as_ref()
1796                        .map(|ident| clap_arg_id_from_field(ident, &f.attrs))
1797                        .unwrap_or_else(|| format!("__f{i}"));
1798                    if is_option_type(&f.ty)
1799                        && field_looks_positional(&f.attrs)
1800                        && !has_clap_mcp_skip(&f.attrs)
1801                        && get_clap_mcp_requires(&f.attrs).is_none()
1802                        && !variant_reqs.contains(&arg_id)
1803                    {
1804                        warn_optional_positional = true;
1805                    }
1806                    if let Some(e) = apply_field_skip(
1807                        &mut skip_args,
1808                        &mut flatten_skip_entries,
1809                        &cmd_name,
1810                        &arg_id,
1811                        &f.attrs,
1812                        &f.ty,
1813                    ) {
1814                        flatten_skip_error = Some(e);
1815                    }
1816                    if field_has_command_flatten(&f.attrs) && variant_has_serialized_args {
1817                        let flat_ty = inner_type_if_option(&f.ty).unwrap_or(&f.ty).clone();
1818                        if matches!(flattened_type_kind(&flat_ty), Ok(FlattenSkipKindTag::Args)) {
1819                            flatten_serialize_topic_cmds.push((cmd_name.clone(), flat_ty));
1820                        }
1821                    }
1822                    if has_clap_mcp_serialize_topic(&f.attrs) {
1823                        serialize_topic_bindings.push((
1824                            cmd_name.clone(),
1825                            arg_id.clone(),
1826                            f.ty.clone(),
1827                        ));
1828                    }
1829                    if let Some(req) = get_clap_mcp_requires(&f.attrs) {
1830                        let req_id = if req.is_empty() { arg_id } else { req };
1831                        requires_args
1832                            .entry(cmd_name.clone())
1833                            .or_default()
1834                            .push(req_id);
1835                    }
1836                }
1837            }
1838        }
1839        syn::Data::Struct(data) => {
1840            let root_name = get_command_name(&input.attrs, name);
1841            let subcommand_field = data
1842                .fields
1843                .iter()
1844                .find(|f| field_has_command_subcommand(&f.attrs));
1845            if let Some(sf) = subcommand_field
1846                && let Some(ref field_ident) = sf.ident
1847                && let Some(e) = apply_field_skip(
1848                    &mut skip_args,
1849                    &mut flatten_skip_entries,
1850                    &root_name,
1851                    &clap_arg_id_from_field(field_ident, &sf.attrs),
1852                    &sf.attrs,
1853                    &sf.ty,
1854                )
1855            {
1856                flatten_skip_error = Some(e);
1857            }
1858            for f in &data.fields {
1859                if subcommand_field.is_some_and(|sf| std::ptr::eq(sf, f)) {
1860                    continue;
1861                }
1862                let Some(ref field_ident) = f.ident else {
1863                    continue;
1864                };
1865                let arg_id = clap_arg_id_from_field(field_ident, &f.attrs);
1866                if is_option_type(&f.ty)
1867                    && field_looks_positional(&f.attrs)
1868                    && !has_clap_mcp_skip(&f.attrs)
1869                    && get_clap_mcp_requires(&f.attrs).is_none()
1870                {
1871                    warn_optional_positional = true;
1872                }
1873                if let Some(e) = apply_field_skip(
1874                    &mut skip_args,
1875                    &mut flatten_skip_entries,
1876                    &root_name,
1877                    &arg_id,
1878                    &f.attrs,
1879                    &f.ty,
1880                ) {
1881                    flatten_skip_error = Some(e);
1882                }
1883                if let Some(req) = get_clap_mcp_requires(&f.attrs) {
1884                    let req_id = if req.is_empty() { arg_id } else { req };
1885                    requires_args
1886                        .entry(root_name.clone())
1887                        .or_default()
1888                        .push(req_id);
1889                }
1890            }
1891            if let Some(sub_field) = subcommand_field {
1892                let sub_ty = inner_type_if_option(&sub_field.ty).unwrap_or(&sub_field.ty);
1893                if let syn::Type::Path(tp) = sub_ty {
1894                    let sub_path = &tp.path;
1895                    let skip_root_assign_local =
1896                        if has_clap_mcp_skip_root_when_subcommands(&input.attrs) {
1897                            quote! { local.skip_root_command_when_subcommands = true; }
1898                        } else {
1899                            quote! {}
1900                        };
1901                    let output_schema_assign_local: proc_macro2::TokenStream = if let Some(types) =
1902                        get_clap_mcp_output_one_of(&input.attrs)
1903                    {
1904                        if types.is_empty() {
1905                            quote! {}
1906                        } else {
1907                            quote! { local.output_schema = clap_mcp::output_schema_one_of!(#(#types),*); }
1908                        }
1909                    } else if let Some(ty) = get_clap_mcp_output_type(&input.attrs) {
1910                        quote! { local.output_schema = clap_mcp::output_schema_for_type::<#ty>(); }
1911                    } else {
1912                        quote! {}
1913                    };
1914                    let skip_root_assign = if has_clap_mcp_skip_root_when_subcommands(&input.attrs)
1915                    {
1916                        quote! { m.skip_root_command_when_subcommands = true; }
1917                    } else {
1918                        quote! {}
1919                    };
1920                    let merge = !skip_commands.is_empty()
1921                        || !skip_args.is_empty()
1922                        || !flatten_skip_entries.is_empty()
1923                        || !requires_args.is_empty()
1924                        || !task_tool_names.is_empty()
1925                        || !serialize_tools.is_empty()
1926                        || !serialize_topic_bindings.is_empty();
1927                    if merge {
1928                        let skip_commands_lit = skip_commands.iter().map(|s| {
1929                            let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
1930                            quote! { #lit.to_string() }
1931                        });
1932                        let task_tool_names_lit = task_tool_names.iter().map(|s| {
1933                            let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
1934                            quote! { #lit.to_string() }
1935                        });
1936                        let skip_args_entries = skip_args.iter().map(|(k, v)| {
1937                            let k_lit = syn::LitStr::new(k, proc_macro2::Span::call_site());
1938                            let vs = v
1939                                .iter()
1940                                .map(|s| {
1941                                    let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
1942                                    quote! { #lit.to_string() }
1943                                });
1944                            quote! {
1945                                local.skip_args.entry(#k_lit.to_string()).or_default().extend([#(#vs),*]);
1946                            }
1947                        });
1948                        let requires_args_entries = requires_args.iter().map(|(k, v)| {
1949                            let k_lit = syn::LitStr::new(k, proc_macro2::Span::call_site());
1950                            let vs = v
1951                                .iter()
1952                                .map(|s| {
1953                                    let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
1954                                    quote! { #lit.to_string() }
1955                                });
1956                            quote! {
1957                                local.requires_args.entry(#k_lit.to_string()).or_default().extend([#(#vs),*]);
1958                            }
1959                        });
1960                        let serialize_tools_entries = serialize_tools.iter().map(|(k, scope)| {
1961                            let k_lit = syn::LitStr::new(k, proc_macro2::Span::call_site());
1962                            match scope {
1963                                ClapMcpSerialized::Tool => quote! {
1964                                    local.serialize_tools.insert(
1965                                        #k_lit.to_string(),
1966                                        clap_mcp::ClapMcpSerializeScope::Tool,
1967                                    );
1968                                },
1969                                ClapMcpSerialized::Args(args) => {
1970                                    let arg_lits = args.iter().map(|s| {
1971                                        let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
1972                                        quote! { #lit.to_string() }
1973                                    });
1974                                    quote! {
1975                                        local.serialize_tools.insert(
1976                                            #k_lit.to_string(),
1977                                            clap_mcp::ClapMcpSerializeScope::Args(vec![#(#arg_lits),*]),
1978                                        );
1979                                    }
1980                                }
1981                            }
1982                        });
1983                        let serialize_topic_entries = serialize_topic_bindings_quote(
1984                            &quote::format_ident!("local"),
1985                            &serialize_topic_bindings,
1986                        );
1987                        let flatten_skip_stmts_local = quote_flatten_skip_stmts(
1988                            &flatten_skip_entries,
1989                            &quote::format_ident!("local"),
1990                        );
1991                        let flatten_topic_stmts_local = flatten_serialize_topic_cmds.iter().map(
1992                            |(cmd, ty)| {
1993                                let cmd_lit = syn::LitStr::new(cmd, proc_macro2::Span::call_site());
1994                                quote! {
1995                                    <#ty as clap_mcp::ClapMcpFlattenArgsTopics>::merge_serialize_topics(
1996                                        #cmd_lit,
1997                                        &mut local.serialize_topic_args,
1998                                    );
1999                                }
2000                            },
2001                        );
2002                        let warn_block = if warn_optional_positional {
2003                            optional_positional_warn_block.clone()
2004                        } else {
2005                            quote! {}
2006                        };
2007                        return quote! {
2008                            impl clap_mcp::ClapMcpSchemaMetadataProvider for #name {
2009                                fn clap_mcp_schema_metadata() -> clap_mcp::ClapMcpSchemaMetadata {
2010                                    #warn_block
2011                                    let mut m = <#sub_path as clap_mcp::ClapMcpSchemaMetadataProvider>::clap_mcp_schema_metadata();
2012                                    let mut local = clap_mcp::ClapMcpSchemaMetadata::default();
2013                                    local.skip_commands.extend([#(#skip_commands_lit),*]);
2014                                    local.task_tool_names.extend([#(#task_tool_names_lit),*]);
2015                                    local.task_augmented_tools = #task_augmented_tools_expr;
2016                                    #(#skip_args_entries)*
2017                                    #(#flatten_skip_stmts_local)*
2018                                    #(#requires_args_entries)*
2019                                    #(#serialize_tools_entries)*
2020                                    #serialize_topic_entries
2021                                    #(#flatten_topic_stmts_local)*
2022                                    #skip_root_assign_local
2023                                    #output_schema_assign_local
2024                                    m.merge_from(local);
2025                                    m
2026                                }
2027                            }
2028                        };
2029                    } else {
2030                        let flatten_skip_stmts_m = quote_flatten_skip_stmts(
2031                            &flatten_skip_entries,
2032                            &quote::format_ident!("m"),
2033                        );
2034                        let warn_block = if warn_optional_positional {
2035                            optional_positional_warn_block.clone()
2036                        } else {
2037                            quote! {}
2038                        };
2039                        return quote! {
2040                            impl clap_mcp::ClapMcpSchemaMetadataProvider for #name {
2041                                fn clap_mcp_schema_metadata() -> clap_mcp::ClapMcpSchemaMetadata {
2042                                    #warn_block
2043                                    let mut m = <#sub_path as clap_mcp::ClapMcpSchemaMetadataProvider>::clap_mcp_schema_metadata();
2044                                    m.task_augmented_tools = m.task_augmented_tools || #task_augmented_tools_expr;
2045                                    #(#flatten_skip_stmts_m)*
2046                                    #skip_root_assign
2047                                    #output_schema_assign
2048                                    m
2049                                }
2050                            }
2051                        };
2052                    }
2053                }
2054            }
2055        }
2056        _ => {}
2057    }
2058
2059    if let Some(e) = flatten_skip_error {
2060        return e.to_compile_error();
2061    }
2062
2063    let skip_commands_lit = skip_commands.iter().map(|s| {
2064        let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
2065        quote! { #lit.to_string() }
2066    });
2067    let skip_args_entries = skip_args.iter().map(|(k, v)| {
2068        let k_lit = syn::LitStr::new(k, proc_macro2::Span::call_site());
2069        let vs = v.iter().map(|s| {
2070            let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
2071            quote! { #lit.to_string() }
2072        });
2073        quote! {
2074            m.skip_args.insert(#k_lit.to_string(), vec![#(#vs),*]);
2075        }
2076    });
2077    let requires_args_entries = requires_args.iter().map(|(k, v)| {
2078        let k_lit = syn::LitStr::new(k, proc_macro2::Span::call_site());
2079        let vs = v.iter().map(|s| {
2080            let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
2081            quote! { #lit.to_string() }
2082        });
2083        quote! {
2084            m.requires_args.insert(#k_lit.to_string(), vec![#(#vs),*]);
2085        }
2086    });
2087    let serialize_tools_entries = serialize_tools.iter().map(|(k, scope)| {
2088        let k_lit = syn::LitStr::new(k, proc_macro2::Span::call_site());
2089        match scope {
2090            ClapMcpSerialized::Tool => quote! {
2091                m.serialize_tools.insert(
2092                    #k_lit.to_string(),
2093                    clap_mcp::ClapMcpSerializeScope::Tool,
2094                );
2095            },
2096            ClapMcpSerialized::Args(args) => {
2097                let arg_lits = args.iter().map(|s| {
2098                    let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
2099                    quote! { #lit.to_string() }
2100                });
2101                quote! {
2102                    m.serialize_tools.insert(
2103                        #k_lit.to_string(),
2104                        clap_mcp::ClapMcpSerializeScope::Args(vec![#(#arg_lits),*]),
2105                    );
2106                }
2107            }
2108        }
2109    });
2110    let serialize_topic_entries =
2111        serialize_topic_bindings_quote(&quote::format_ident!("m"), &serialize_topic_bindings);
2112    let flatten_skip_stmts =
2113        quote_flatten_skip_stmts(&flatten_skip_entries, &quote::format_ident!("m"));
2114    let flatten_topic_stmts = flatten_serialize_topic_cmds.iter().map(|(cmd, ty)| {
2115        let cmd_lit = syn::LitStr::new(cmd, proc_macro2::Span::call_site());
2116        quote! {
2117            <#ty as clap_mcp::ClapMcpFlattenArgsTopics>::merge_serialize_topics(
2118                #cmd_lit,
2119                &mut m.serialize_topic_args,
2120            );
2121        }
2122    });
2123    let task_tool_names_lit = task_tool_names.iter().map(|s| {
2124        let lit = syn::LitStr::new(s, proc_macro2::Span::call_site());
2125        quote! { #lit.to_string() }
2126    });
2127
2128    let warn_block = if warn_optional_positional {
2129        optional_positional_warn_block
2130    } else {
2131        quote! {}
2132    };
2133
2134    let nested_merge_stmts = match &input.data {
2135        syn::Data::Enum(data) => {
2136            let paths = nested_subcommand_type_paths_from_enum(data);
2137            paths.iter().map(|p| {
2138                quote! { m.merge_from(<#p as clap_mcp::ClapMcpSchemaMetadataProvider>::clap_mcp_schema_metadata()); }
2139            }).collect::<Vec<_>>()
2140        }
2141        _ => Vec::new(),
2142    };
2143
2144    quote! {
2145        impl clap_mcp::ClapMcpSchemaMetadataProvider for #name {
2146            fn clap_mcp_schema_metadata() -> clap_mcp::ClapMcpSchemaMetadata {
2147                #warn_block
2148                let mut m = clap_mcp::ClapMcpSchemaMetadata::default();
2149                #(#nested_merge_stmts)*
2150                m.skip_commands.extend([#(#skip_commands_lit),*]);
2151                m.task_tool_names.extend([#(#task_tool_names_lit),*]);
2152                m.task_augmented_tools = m.task_augmented_tools || #task_augmented_tools_expr;
2153                #(#skip_args_entries)*
2154                #(#flatten_skip_stmts)*
2155                #(#requires_args_entries)*
2156                #(#serialize_tools_entries)*
2157                #serialize_topic_entries
2158                #(#flatten_topic_stmts)*
2159                #output_schema_assign
2160                m
2161            }
2162        }
2163    }
2164}