feat(server): add annotation to tool macro (#184)

skip serializing the tool annotations if none, fixes mcp inspector
This commit is contained in:
Abhay Agarwal 2025-05-18 01:04:58 -07:00 • committed by GitHub
commit 68ddea3ad5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 124 additions and 3 deletions

View file

@ -18,6 +18,7 @@ proc-macro = true
syn = {version = "2", features = ["full"]}
quote = "1"
proc-macro2 = "1"
serde_json = "1.0"
[features]

View file

@ -2,11 +2,45 @@ use std::collections::HashSet;
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use serde_json::json;
use syn::{
Expr, FnArg, Ident, ItemFn, ItemImpl, MetaList, PatType, Token, Type, Visibility, parse::Parse,
parse_quote, spanned::Spanned,
Expr, FnArg, Ident, ItemFn, ItemImpl, Lit, MetaList, PatType, Token, Type, Visibility,
parse::Parse, parse_quote, spanned::Spanned,
};
/// Stores tool annotation attributes
#[derive(Default, Clone)]
struct ToolAnnotationAttrs(pub serde_json::Map<String, serde_json::Value>);
impl Parse for ToolAnnotationAttrs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut attrs = serde_json::Map::new();
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![:]>()?;
let value: Lit = input.parse()?;
let value = match value {
Lit::Str(s) => json!(s.value()),
Lit::Bool(b) => json!(b.value),
_ => {
return Err(syn::Error::new(
key.span(),
"annotations must be string or boolean literals",
));
}
};
attrs.insert(key.to_string(), value);
if input.is_empty() {
break;
}
input.parse::<Token![,]>()?;
}
Ok(ToolAnnotationAttrs(attrs))
}
}
#[derive(Default)]
struct ToolImplItemAttrs {
tool_box: Option<Option<Ident>>,
@ -45,6 +79,7 @@ struct ToolFnItemAttrs {
name: Option<Expr>,
description: Option<Expr>,
vis: Option<Visibility>,
annotations: Option<ToolAnnotationAttrs>,
}
impl Parse for ToolFnItemAttrs {
@ -52,6 +87,8 @@ impl Parse for ToolFnItemAttrs {
let mut name = None;
let mut description = None;
let mut vis = None;
let mut annotations = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
@ -68,6 +105,13 @@ impl Parse for ToolFnItemAttrs {
let value: Visibility = input.parse()?;
vis = Some(value);
}
"annotations" => {
// Parse the annotations as a nested structure
let content;
syn::braced!(content in input);
let value = content.parse()?;
annotations = Some(value);
}
_ => {
return Err(syn::Error::new(key.span(), "unknown attribute"));
}
@ -82,6 +126,7 @@ impl Parse for ToolFnItemAttrs {
name,
description,
vis,
annotations,
})
}
}
@ -470,6 +515,17 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
};
let input_fn_attrs = &input_fn.attrs;
let input_fn_vis = &input_fn.vis;
let annotations_code = if let Some(annotations) = &tool_macro_attrs.fn_item.annotations {
let annotations =
serde_json::to_string(&annotations.0).expect("failed to serialize annotations");
quote! {
Some(serde_json::from_str::<rmcp::model::ToolAnnotations>(&#annotations).expect("Could not parse tool annotations"))
}
} else {
quote! { None }
};
quote! {
#(#input_fn_attrs)*
#input_fn_vis fn #tool_attr_fn_ident() -> rmcp::model::Tool {
@ -477,7 +533,7 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu
name: #name.into(),
description: Some(#description.into()),
input_schema: #schema.into(),
annotations: None
annotations: #annotations_code,
}
}
}