1use proc_macro::TokenStream;
4use proc_macro_crate::{FoundCrate, crate_name};
5use quote::{format_ident, quote};
6use std::collections::BTreeSet;
7use syn::{
8 Data, DeriveInput, Field, Fields, Ident, LitStr, Result, Type, ext::IdentExt,
9 meta::ParseNestedMeta, parse_macro_input, parse_quote,
10};
11
12#[cfg(test)]
13use {libmcp as _, trybuild as _};
14
15const LIBMCP_ATTR: &str = "libmcp";
16const ATTR_KIND: &str = "kind";
17const ATTR_REFERENCE_ONLY: &str = "reference_only";
18const ATTR_ALLOW_OPAQUE_IDS: &str = "allow_opaque_ids";
19const FIELD_SELECTOR: &str = "selector";
20const FIELD_TITLE: &str = "title";
21const FIELD_SLUG: &str = "slug";
22const FIELD_SKIP: &str = "skip";
23const FIELD_SKIP_NONE: &str = "skip_none";
24const FIELD_FULL_ONLY: &str = "full_only";
25const FIELD_FULL: &str = "full";
26const FIELD_CONCISE_ONLY: &str = "concise_only";
27
28#[proc_macro_derive(ToolProjection, attributes(libmcp))]
30pub fn derive_tool_projection(input: TokenStream) -> TokenStream {
31 expand_tool_projection(parse_macro_input!(input as DeriveInput))
32 .unwrap_or_else(syn::Error::into_compile_error)
33 .into()
34}
35
36#[proc_macro_derive(SelectorProjection, attributes(libmcp))]
38pub fn derive_selector_projection(input: TokenStream) -> TokenStream {
39 expand_selector_projection(parse_macro_input!(input as DeriveInput))
40 .unwrap_or_else(syn::Error::into_compile_error)
41 .into()
42}
43
44fn expand_tool_projection(input: DeriveInput) -> Result<proc_macro2::TokenStream> {
45 let DeriveInput {
46 attrs,
47 ident,
48 mut generics,
49 data,
50 ..
51 } = input;
52 let fields = named_fields(data, &ident, "ToolProjection")?;
53 let container = parse_container_attrs(&attrs)?;
54 let parsed_fields = fields
55 .iter()
56 .map(|field| parse_field_attrs(field).map(|attrs| (field, attrs)))
57 .collect::<Result<Vec<_>>>()?;
58 let libmcp = libmcp_path()?;
59
60 for (field, attrs) in &parsed_fields {
61 if attrs.projected() {
62 let ty = &field.ty;
63 generics
64 .make_where_clause()
65 .predicates
66 .push(parse_quote!(#ty: #libmcp::__macro::serde::Serialize));
67 }
68 }
69
70 let concise_entries = parsed_fields
71 .iter()
72 .filter(|(_, attrs)| attrs.in_concise())
73 .map(|(field, attrs)| project_field(field, attrs, &libmcp))
74 .collect::<Result<Vec<_>>>()?;
75 let full_entries = parsed_fields
76 .iter()
77 .filter(|(_, attrs)| attrs.in_full())
78 .map(|(field, attrs)| project_field(field, attrs, &libmcp))
79 .collect::<Result<Vec<_>>>()?;
80 let kind = container.kind.tokens(&libmcp);
81 let reference_only = container.reference_only;
82 let forbid_opaque_ids = !container.allow_opaque_ids;
83 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
84
85 Ok(quote! {
86 impl #impl_generics #libmcp::StructuredProjection for #ident #ty_generics #where_clause {
87 fn concise_projection(
88 &self,
89 ) -> ::std::result::Result<
90 #libmcp::__macro::serde_json::Value,
91 #libmcp::ProjectionError,
92 > {
93 let mut object = #libmcp::__macro::serde_json::Map::new();
94 #(#concise_entries)*
95 Ok(#libmcp::__macro::serde_json::Value::Object(object))
96 }
97
98 fn full_projection(
99 &self,
100 ) -> ::std::result::Result<
101 #libmcp::__macro::serde_json::Value,
102 #libmcp::ProjectionError,
103 > {
104 let mut object = #libmcp::__macro::serde_json::Map::new();
105 #(#full_entries)*
106 Ok(#libmcp::__macro::serde_json::Value::Object(object))
107 }
108 }
109
110 impl #impl_generics #libmcp::SurfacePolicy for #ident #ty_generics #where_clause {
111 fn projection_policy(&self) -> #libmcp::ProjectionPolicy {
112 #libmcp::ProjectionPolicy::from_surface(
113 #kind,
114 #forbid_opaque_ids,
115 #reference_only,
116 )
117 }
118 }
119 })
120}
121
122fn expand_selector_projection(input: DeriveInput) -> Result<proc_macro2::TokenStream> {
123 let DeriveInput {
124 attrs,
125 ident,
126 mut generics,
127 data,
128 ..
129 } = input;
130 let _container = parse_container_attrs(&attrs)?;
131 let fields = named_fields(data, &ident, "SelectorProjection")?;
132 let parsed_fields = fields
133 .iter()
134 .map(|field| parse_field_attrs(field).map(|attrs| (field, attrs)))
135 .collect::<Result<Vec<_>>>()?;
136 let libmcp = libmcp_path()?;
137
138 let slug_field = unique_marked_field(
139 &parsed_fields,
140 |attrs| attrs.has(FieldFlag::Selector),
141 "selector",
142 )?
143 .or_else(|| {
144 parsed_fields
145 .iter()
146 .find(|(field, _)| field_ident(field) == FIELD_SLUG)
147 .map(|(field, _)| *field)
148 })
149 .ok_or_else(|| syn::Error::new_spanned(&ident, "SelectorProjection needs a slug field"))?;
150 let title_field =
151 unique_marked_field(&parsed_fields, |attrs| attrs.has(FieldFlag::Title), "title")?.or_else(
152 || {
153 parsed_fields
154 .iter()
155 .find(|(field, _)| field_ident(field) == FIELD_TITLE)
156 .map(|(field, _)| *field)
157 },
158 );
159
160 let slug_ident = named_ident(slug_field, "SelectorProjection needs named fields")?;
161 let slug_ty = &slug_field.ty;
162 generics.make_where_clause().predicates.push(parse_quote!(
163 #slug_ty: ::std::clone::Clone + ::std::convert::Into<::std::string::String>
164 ));
165 let title_tokens = if let Some(field) = title_field {
166 let title_ident = named_ident(field, "SelectorProjection needs named fields")?;
167 let title_ty = &field.ty;
168 generics.make_where_clause().predicates.push(parse_quote!(
169 #title_ty: ::std::clone::Clone + ::std::convert::Into<::std::string::String>
170 ));
171 quote! {
172 title: ::std::option::Option::Some(self.#title_ident.clone().into())
173 }
174 } else {
175 quote! { title: ::std::option::Option::None }
176 };
177 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
178
179 Ok(quote! {
180 impl #impl_generics #libmcp::SelectorProjection for #ident #ty_generics #where_clause {
181 fn selector_ref(&self) -> #libmcp::SelectorRef {
182 #libmcp::SelectorRef {
183 slug: self.#slug_ident.clone().into(),
184 #title_tokens,
185 }
186 }
187 }
188 })
189}
190
191fn named_fields(data: Data, ident: &Ident, derive: &str) -> Result<Vec<Field>> {
192 let data = match data {
193 Data::Struct(data) => data,
194 Data::Enum(_) | Data::Union(_) => {
195 return Err(syn::Error::new_spanned(
196 ident,
197 format!("{derive} only supports structs"),
198 ));
199 }
200 };
201 match data.fields {
202 Fields::Named(fields) => Ok(fields.named.into_iter().collect()),
203 Fields::Unnamed(_) | Fields::Unit => Err(syn::Error::new_spanned(
204 ident,
205 format!("{derive} requires named fields"),
206 )),
207 }
208}
209
210fn project_field(
211 field: &Field,
212 attrs: &FieldAttrs,
213 libmcp: &proc_macro2::TokenStream,
214) -> Result<proc_macro2::TokenStream> {
215 let ident = named_ident(field, "ToolProjection requires named fields")?;
216 let key = LitStr::new(field_ident(field).as_str(), ident.span());
217 if attrs.has(FieldFlag::SkipNone) {
218 Ok(quote! {
219 if let ::std::option::Option::Some(value) = &self.#ident {
220 object.insert(
221 #key.to_owned(),
222 #libmcp::__macro::serde_json::to_value(value)
223 .map_err(#libmcp::ProjectionError::from)?,
224 );
225 }
226 })
227 } else {
228 Ok(quote! {
229 object.insert(
230 #key.to_owned(),
231 #libmcp::__macro::serde_json::to_value(&self.#ident)
232 .map_err(#libmcp::ProjectionError::from)?,
233 );
234 })
235 }
236}
237
238fn named_ident<'a>(field: &'a Field, message: &str) -> Result<&'a Ident> {
239 field
240 .ident
241 .as_ref()
242 .ok_or_else(|| syn::Error::new_spanned(field, message))
243}
244
245fn field_ident(field: &Field) -> String {
246 field
247 .ident
248 .as_ref()
249 .map_or_else(String::new, |ident| ident.unraw().to_string())
250}
251
252fn unique_marked_field<'a>(
253 fields: &'a [(&'a Field, FieldAttrs)],
254 marked: impl Fn(&FieldAttrs) -> bool,
255 marker: &str,
256) -> Result<Option<&'a Field>> {
257 let mut matches = fields
258 .iter()
259 .filter(|(_, attrs)| marked(attrs))
260 .map(|(field, _)| *field);
261 let first = matches.next();
262 if let Some(duplicate) = matches.next() {
263 return Err(syn::Error::new_spanned(
264 duplicate,
265 format!("only one field may be marked {marker}"),
266 ));
267 }
268 Ok(first)
269}
270
271#[derive(Default)]
272struct ContainerAttrs {
273 kind: SurfaceKindAttr,
274 kind_seen: bool,
275 reference_only: bool,
276 allow_opaque_ids: bool,
277}
278
279fn parse_container_attrs(attrs: &[syn::Attribute]) -> Result<ContainerAttrs> {
280 let mut parsed = ContainerAttrs::default();
281 for attr in attrs
282 .iter()
283 .filter(|attr| attr.path().is_ident(LIBMCP_ATTR))
284 {
285 attr.parse_nested_meta(|meta| {
286 if meta.path.is_ident(ATTR_KIND) {
287 if parsed.kind_seen {
288 return Err(meta.error("duplicate libmcp kind"));
289 }
290 let kind = meta.value()?.parse::<LitStr>()?;
291 parsed.kind = SurfaceKindAttr::parse(&kind)?;
292 parsed.kind_seen = true;
293 return Ok(());
294 }
295 if meta.path.is_ident(ATTR_REFERENCE_ONLY) {
296 return set_flag(&mut parsed.reference_only, &meta);
297 }
298 if meta.path.is_ident(ATTR_ALLOW_OPAQUE_IDS) {
299 return set_flag(&mut parsed.allow_opaque_ids, &meta);
300 }
301 Err(meta.error("unsupported libmcp container attribute"))
302 })?;
303 }
304 Ok(parsed)
305}
306
307#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
308enum FieldFlag {
309 Selector,
310 Title,
311 Skip,
312 SkipNone,
313 FullOnly,
314 ConciseOnly,
315}
316
317#[derive(Default)]
318struct FieldAttrs {
319 flags: BTreeSet<FieldFlag>,
320}
321
322impl FieldAttrs {
323 fn has(&self, flag: FieldFlag) -> bool {
324 self.flags.contains(&flag)
325 }
326
327 fn projected(&self) -> bool {
328 !self.has(FieldFlag::Skip)
329 }
330
331 fn in_concise(&self) -> bool {
332 self.projected() && !self.has(FieldFlag::FullOnly)
333 }
334
335 fn in_full(&self) -> bool {
336 self.projected() && !self.has(FieldFlag::ConciseOnly)
337 }
338}
339
340fn parse_field_attrs(field: &Field) -> Result<FieldAttrs> {
341 let mut parsed = FieldAttrs::default();
342 for attr in field
343 .attrs
344 .iter()
345 .filter(|attr| attr.path().is_ident(LIBMCP_ATTR))
346 {
347 attr.parse_nested_meta(|meta| {
348 let flag = if meta.path.is_ident(FIELD_SELECTOR) {
349 FieldFlag::Selector
350 } else if meta.path.is_ident(FIELD_TITLE) {
351 FieldFlag::Title
352 } else if meta.path.is_ident(FIELD_SKIP) {
353 FieldFlag::Skip
354 } else if meta.path.is_ident(FIELD_SKIP_NONE) {
355 FieldFlag::SkipNone
356 } else if meta.path.is_ident(FIELD_FULL_ONLY) || meta.path.is_ident(FIELD_FULL) {
357 FieldFlag::FullOnly
358 } else if meta.path.is_ident(FIELD_CONCISE_ONLY) {
359 FieldFlag::ConciseOnly
360 } else {
361 return Err(meta.error("unsupported libmcp field attribute"));
362 };
363 insert_field_flag(&mut parsed.flags, flag, &meta)
364 })?;
365 }
366
367 if parsed.has(FieldFlag::Skip)
368 && [
369 FieldFlag::SkipNone,
370 FieldFlag::FullOnly,
371 FieldFlag::ConciseOnly,
372 ]
373 .into_iter()
374 .any(|flag| parsed.has(flag))
375 {
376 return Err(syn::Error::new_spanned(
377 field,
378 "skip cannot be combined with projection detail attributes",
379 ));
380 }
381 if parsed.has(FieldFlag::FullOnly) && parsed.has(FieldFlag::ConciseOnly) {
382 return Err(syn::Error::new_spanned(
383 field,
384 "full_only and concise_only are mutually exclusive",
385 ));
386 }
387 if parsed.has(FieldFlag::SkipNone) && !is_option(&field.ty) {
388 return Err(syn::Error::new_spanned(
389 &field.ty,
390 "skip_none requires an Option field",
391 ));
392 }
393 if parsed.has(FieldFlag::Selector) && parsed.has(FieldFlag::Title) {
394 return Err(syn::Error::new_spanned(
395 field,
396 "selector and title must identify different fields",
397 ));
398 }
399 Ok(parsed)
400}
401
402fn insert_field_flag(
403 flags: &mut BTreeSet<FieldFlag>,
404 flag: FieldFlag,
405 meta: &ParseNestedMeta<'_>,
406) -> Result<()> {
407 if meta.input.peek(syn::Token![=]) || meta.input.peek(syn::token::Paren) {
408 return Err(meta.error("libmcp flag does not take a value"));
409 }
410 if !flags.insert(flag) {
411 return Err(meta.error("duplicate libmcp flag"));
412 }
413 Ok(())
414}
415
416fn set_flag(slot: &mut bool, meta: &ParseNestedMeta<'_>) -> Result<()> {
417 if meta.input.peek(syn::Token![=]) || meta.input.peek(syn::token::Paren) {
418 return Err(meta.error("libmcp flag does not take a value"));
419 }
420 if std::mem::replace(slot, true) {
421 return Err(meta.error("duplicate libmcp flag"));
422 }
423 Ok(())
424}
425
426fn is_option(ty: &Type) -> bool {
427 matches!(
428 ty,
429 Type::Path(path) if path.qself.is_none()
430 && path.path.segments.last().is_some_and(|segment| segment.ident == "Option")
431 )
432}
433
434#[derive(Default)]
435enum SurfaceKindAttr {
436 Overview,
437 List,
438 #[default]
439 Read,
440 Mutation,
441 Ops,
442}
443
444impl SurfaceKindAttr {
445 fn parse(kind: &LitStr) -> Result<Self> {
446 match kind.value().trim().to_ascii_lowercase().as_str() {
447 "overview" => Ok(Self::Overview),
448 "list" => Ok(Self::List),
449 "read" => Ok(Self::Read),
450 "mutation" => Ok(Self::Mutation),
451 "ops" => Ok(Self::Ops),
452 _ => Err(syn::Error::new_spanned(
453 kind,
454 "kind must be overview, list, read, mutation, or ops",
455 )),
456 }
457 }
458
459 fn tokens(&self, libmcp: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
460 let variant = match self {
461 Self::Overview => quote!(Overview),
462 Self::List => quote!(List),
463 Self::Read => quote!(Read),
464 Self::Mutation => quote!(Mutation),
465 Self::Ops => quote!(Ops),
466 };
467 quote!(#libmcp::SurfaceKind::#variant)
468 }
469}
470
471fn libmcp_path() -> Result<proc_macro2::TokenStream> {
472 match crate_name("libmcp") {
473 Ok(FoundCrate::Itself) => Ok(quote!(crate)),
474 Ok(FoundCrate::Name(name)) => {
475 let ident = format_ident!("{name}");
476 Ok(quote!(::#ident))
477 }
478 Err(error) => Err(syn::Error::new(
479 proc_macro2::Span::call_site(),
480 format!("failed to locate libmcp crate: {error}"),
481 )),
482 }
483}