1use 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
13type 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
38fn 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); }
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); }
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); }
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); }
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); }
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); }
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
146fn 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
166fn 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
186fn 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
208enum ClapMcpSerialized {
210 Tool,
211 Args(Vec<String>),
212}
213
214fn 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
257fn 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
295fn 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
322fn 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
659fn 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
680fn 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
711fn 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
761fn 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
778fn 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
799fn 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()); }
819 }
820 Ok(())
821 });
822 if result.is_some() {
823 return result;
824 }
825 }
826 None
827}
828
829fn 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
849fn 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
869fn field_looks_positional(attrs: &[syn::Attribute]) -> bool {
871 field_has_arg_index(attrs) || !field_has_arg_long_or_short(attrs)
872}
873
874fn 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
933fn 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#[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
1709fn 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
1730fn 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 "e::format_ident!("local"),
1985 &serialize_topic_bindings,
1986 );
1987 let flatten_skip_stmts_local = quote_flatten_skip_stmts(
1988 &flatten_skip_entries,
1989 "e::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 "e::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("e::format_ident!("m"), &serialize_topic_bindings);
2112 let flatten_skip_stmts =
2113 quote_flatten_skip_stmts(&flatten_skip_entries, "e::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}