feat: Add prompt support (#351)

* feat: add prompt support with typed argument handling

- Implement #[prompt], #[prompt_router], and #[prompt_handler] macros
- Add automatic JSON schema generation from Rust types for arguments
- Support flexible async handler signatures with automatic adaptation
- Create PromptRouter for efficient prompt dispatch
- Include comprehensive tests and example implementation

This enables MCP servers to provide reusable prompt templates that
LLMs can discover and invoke with strongly-typed parameters, similar
to the existing tool system but optimized for prompt use cases.

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: unify parameter handling between tools and prompts

- Replace Arguments<T> with Parameters<T> for consistent API
- Create shared common module for tool/prompt utilities
- Modernize async handling with futures::future::BoxFuture
- Move cached_schema_for_type to common module for reuse
- Update error types from rmcp::Error to rmcp::ErrorData
- Add comprehensive trait implementations for parameter extraction

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: extract Parameters wrapper to shared module and unify trait usage

- Move Parameters type from common.rs to dedicated wrapper/parameters.rs module
- Unify extractor traits by using FromContextPart for both tools and prompts
- Remove duplicate FromToolCallContextPart and FromPromptContextPart traits
- Add structured output support to Json wrapper with IntoCallToolResult impl
- Improve error messages with more descriptive panic for schema serialization
- Update all imports across codebase to use new module path
- Clean up trailing whitespace and formatting inconsistencies

This consolidates parameter extraction logic and reduces code duplication
between tool and prompt handlers while maintaining backward compatibility.

* chore: remove committed .egg-info directory and update gitignore

* docs: fix documentation formatting
This commit is contained in:
Michael Assaf 2025-08-20 23:22:19 -04:00 • committed by GitHub
commit 9349f5cb26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 3191 additions and 464 deletions

View file

@ -0,0 +1,65 @@
//! Common utilities shared between different macro implementations
use quote::quote;
use syn::{Attribute, Expr, FnArg, ImplItemFn, Signature, Type};
/// Parse a None expression
pub fn none_expr() -> syn::Result<Expr> {
syn::parse2::<Expr>(quote! { None })
}
/// Extract documentation from doc attributes
pub fn extract_doc_line(existing_docs: Option<String>, attr: &Attribute) -> Option<String> {
if !attr.path().is_ident("doc") {
return None;
}
let syn::Meta::NameValue(name_value) = &attr.meta else {
return None;
};
let syn::Expr::Lit(expr_lit) = &name_value.value else {
return None;
};
let syn::Lit::Str(lit_str) = &expr_lit.lit else {
return None;
};
let content = lit_str.value().trim().to_string();
match (existing_docs, content) {
(Some(mut existing_docs), content) if !content.is_empty() => {
existing_docs.push('\n');
existing_docs.push_str(&content);
Some(existing_docs)
}
(Some(existing_docs), _) => Some(existing_docs),
(None, content) if !content.is_empty() => Some(content),
_ => None,
}
}
/// Find Parameters<T> type in function signature
/// Returns the full Parameters<T> type if found
pub fn find_parameters_type_in_sig(sig: &Signature) -> Option<Box<Type>> {
sig.inputs.iter().find_map(|input| {
if let FnArg::Typed(pat_type) = input {
if let Type::Path(type_path) = &*pat_type.ty {
if type_path
.path
.segments
.last()
.is_some_and(|type_name| type_name.ident == "Parameters")
{
return Some(pat_type.ty.clone());
}
}
}
None
})
}
/// Find Parameters<T> type in ImplItemFn
pub fn find_parameters_type_impl(fn_item: &ImplItemFn) -> Option<Box<Type>> {
find_parameters_type_in_sig(&fn_item.sig)
}

View file

@ -1,6 +1,10 @@
#[allow(unused_imports)]
use proc_macro::TokenStream;
mod common;
mod prompt;
mod prompt_handler;
mod prompt_router;
mod tool;
mod tool_handler;
mod tool_router;
@ -160,3 +164,102 @@ pub fn tool_handler(attr: TokenStream, input: TokenStream) -> TokenStream {
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// # prompt
///
/// This macro is used to mark a function as a prompt handler.
///
/// This will generate a function that returns the attribute of this prompt, with type `rmcp::model::Prompt`.
///
/// ## Usage
///
/// | field | type | usage |
/// | :- | :- | :- |
/// | `name` | `String` | The name of the prompt. If not provided, it defaults to the function name. |
/// | `description` | `String` | A description of the prompt. The document of this function will be used if not provided. |
/// | `arguments` | `Expr` | An expression that evaluates to `Option<Vec<PromptArgument>>` defining the prompt's arguments. If not provided, it will automatically generate arguments from the `Parameters<T>` type found in the function signature. |
///
/// ## Example
///
/// ```rust,ignore
/// #[prompt(name = "code_review", description = "Reviews code for best practices")]
/// pub async fn code_review_prompt(&self, Parameters(args): Parameters<CodeReviewArgs>) -> Result<Vec<PromptMessage>> {
/// // Generate prompt messages based on arguments
/// }
/// ```
#[proc_macro_attribute]
pub fn prompt(attr: TokenStream, input: TokenStream) -> TokenStream {
prompt::prompt(attr.into(), input.into())
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// # prompt_router
///
/// This macro generates a prompt router based on functions marked with `#[rmcp::prompt]` in an implementation block.
///
/// It creates a function that returns a `PromptRouter` instance.
///
/// ## Usage
///
/// | field | type | usage |
/// | :- | :- | :- |
/// | `router` | `Ident` | The name of the router function to be generated. Defaults to `prompt_router`. |
/// | `vis` | `Visibility` | The visibility of the generated router function. Defaults to empty. |
///
/// ## Example
///
/// ```rust,ignore
/// #[prompt_router]
/// impl MyPromptHandler {
/// #[prompt]
/// pub async fn greeting_prompt(&self, Parameters(args): Parameters<GreetingArgs>) -> Result<Vec<PromptMessage>, Error> {
/// // Generate greeting prompt using args
/// }
///
/// pub fn new() -> Self {
/// Self {
/// // the default name of prompt router will be `prompt_router`
/// prompt_router: Self::prompt_router(),
/// }
/// }
/// }
/// ```
#[proc_macro_attribute]
pub fn prompt_router(attr: TokenStream, input: TokenStream) -> TokenStream {
prompt_router::prompt_router(attr.into(), input.into())
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// # prompt_handler
///
/// This macro generates handler methods for `get_prompt` and `list_prompts` in the implementation block, using an existing `PromptRouter` instance.
///
/// ## Usage
///
/// | field | type | usage |
/// | :- | :- | :- |
/// | `router` | `Expr` | The expression to access the `PromptRouter` instance. Defaults to `self.prompt_router`. |
///
/// ## Example
/// ```rust,ignore
/// #[prompt_handler]
/// impl ServerHandler for MyPromptHandler {
/// // ...implement other handler methods
/// }
/// ```
///
/// or using a custom router expression:
/// ```rust,ignore
/// #[prompt_handler(router = self.get_prompt_router())]
/// impl ServerHandler for MyPromptHandler {
/// // ...implement other handler methods
/// }
/// ```
#[proc_macro_attribute]
pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> TokenStream {
prompt_handler::prompt_handler(attr.into(), input.into())
.unwrap_or_else(|err| err.to_compile_error())
.into()
}

View file

@ -0,0 +1,181 @@
use darling::{FromMeta, ast::NestedMeta};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Expr, Ident, ImplItemFn, ReturnType};
use crate::common::{extract_doc_line, none_expr};
#[derive(FromMeta, Default, Debug)]
#[darling(default)]
pub struct PromptAttribute {
/// The name of the prompt
pub name: Option<String>,
/// Optional description of what the prompt does
pub description: Option<String>,
/// Arguments that can be passed to the prompt
pub arguments: Option<Expr>,
}
pub struct ResolvedPromptAttribute {
pub name: String,
pub description: Option<String>,
pub arguments: Expr,
}
impl ResolvedPromptAttribute {
pub fn into_fn(self, fn_ident: Ident) -> syn::Result<ImplItemFn> {
let Self {
name,
description,
arguments,
} = self;
let description = if let Some(description) = description {
quote! { Some(#description.into()) }
} else {
quote! { None }
};
let tokens = quote! {
pub fn #fn_ident() -> rmcp::model::Prompt {
rmcp::model::Prompt {
name: #name.into(),
description: #description,
arguments: #arguments,
}
}
};
syn::parse2::<ImplItemFn>(tokens)
}
}
pub fn prompt(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
let attribute = if attr.is_empty() {
Default::default()
} else {
let attr_args = NestedMeta::parse_meta_list(attr)?;
PromptAttribute::from_list(&attr_args)?
};
let mut fn_item = syn::parse2::<ImplItemFn>(input.clone())?;
let fn_ident = &fn_item.sig.ident;
let prompt_attr_fn_ident = format_ident!("{}_prompt_attr", fn_ident);
// Try to find prompt parameters from function parameters
let arguments_expr = if let Some(arguments) = attribute.arguments {
arguments
} else {
// Look for a type named Parameters in the function signature
let params_ty = crate::common::find_parameters_type_impl(&fn_item);
if let Some(params_ty) = params_ty {
// Generate arguments from the type's schema with caching
syn::parse2::<Expr>(quote! {
rmcp::handler::server::prompt::cached_arguments_from_schema::<#params_ty>()
})?
} else {
// No arguments
none_expr()?
}
};
let name = attribute.name.unwrap_or_else(|| fn_ident.to_string());
let description = attribute
.description
.or_else(|| fn_item.attrs.iter().fold(None, extract_doc_line));
let arguments = arguments_expr;
let resolved_prompt_attr = ResolvedPromptAttribute {
name: name.clone(),
description: description.clone(),
arguments: arguments.clone(),
};
let prompt_attr_fn = resolved_prompt_attr.into_fn(prompt_attr_fn_ident.clone())?;
// Modify the input function for async support (same as tool macro)
if fn_item.sig.asyncness.is_some() {
// 1. remove asyncness from sig
// 2. make return type: `futures::future::BoxFuture<'_, #ReturnType>`
// 3. make body: { Box::pin(async move { #body }) }
let new_output = syn::parse2::<ReturnType>({
let mut lt = quote! { 'static };
if let Some(receiver) = fn_item.sig.receiver() {
if let Some((_, receiver_lt)) = receiver.reference.as_ref() {
if let Some(receiver_lt) = receiver_lt {
lt = quote! { #receiver_lt };
} else {
lt = quote! { '_ };
}
}
}
match &fn_item.sig.output {
syn::ReturnType::Default => {
quote! { -> futures::future::BoxFuture<#lt, ()> }
}
syn::ReturnType::Type(_, ty) => {
quote! { -> futures::future::BoxFuture<#lt, #ty> }
}
}
})?;
let prev_block = &fn_item.block;
let new_block = syn::parse2::<syn::Block>(quote! {
{ Box::pin(async move #prev_block ) }
})?;
fn_item.sig.asyncness = None;
fn_item.sig.output = new_output;
fn_item.block = new_block;
}
Ok(quote! {
#prompt_attr_fn
#fn_item
})
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_prompt_macro() -> syn::Result<()> {
let attr = quote! {
name = "example-prompt",
description = "An example prompt"
};
let input = quote! {
async fn example_prompt(&self, Parameters(args): Parameters<ExampleArgs>) -> Result<String> {
Ok("Example prompt response".to_string())
}
};
let result = prompt(attr, input)?;
// Verify the output contains both the attribute function and the modified function
let result_str = result.to_string();
assert!(result_str.contains("example_prompt_prompt_attr"));
assert!(
result_str.contains("rmcp")
&& result_str.contains("model")
&& result_str.contains("Prompt")
);
Ok(())
}
#[test]
fn test_doc_comment_description() -> syn::Result<()> {
let attr = quote! {}; // No explicit description
let input = quote! {
/// This is a test prompt description
/// with multiple lines
fn test_prompt(&self) -> Result<String> {
Ok("Test".to_string())
}
};
let result = prompt(attr, input)?;
// The output should contain the description from doc comments
let result_str = result.to_string();
assert!(result_str.contains("This is a test prompt description"));
assert!(result_str.contains("with multiple lines"));
Ok(())
}
}

View file

@ -0,0 +1,141 @@
use darling::FromMeta;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Expr, ImplItem, ItemImpl, parse_quote};
#[derive(FromMeta, Debug, Default)]
#[darling(default)]
pub struct PromptHandlerAttribute {
pub router: Option<Expr>,
}
pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
let attribute = if attr.is_empty() {
Default::default()
} else {
let attr_args = darling::ast::NestedMeta::parse_meta_list(attr)?;
PromptHandlerAttribute::from_list(&attr_args)?
};
let mut impl_block = syn::parse2::<ItemImpl>(input)?;
let router_expr = attribute
.router
.unwrap_or_else(|| syn::parse2(quote! { self.prompt_router }).unwrap());
// Add get_prompt implementation
let get_prompt_impl: ImplItem = parse_quote! {
async fn get_prompt(
&self,
request: GetPromptRequestParam,
context: RequestContext<RoleServer>,
) -> Result<GetPromptResult, rmcp::ErrorData> {
let prompt_context = rmcp::handler::server::prompt::PromptContext::new(
self,
request.name,
request.arguments,
context,
);
#router_expr.get_prompt(prompt_context).await
}
};
// Add list_prompts implementation
let list_prompts_impl: ImplItem = parse_quote! {
async fn list_prompts(
&self,
_request: Option<PaginatedRequestParam>,
_context: RequestContext<RoleServer>,
) -> Result<ListPromptsResult, rmcp::ErrorData> {
let prompts = #router_expr.list_all();
Ok(ListPromptsResult {
prompts,
next_cursor: None,
})
}
};
// Check if methods already exist and replace them if they do
let mut has_get_prompt = false;
let mut has_list_prompts = false;
for item in &mut impl_block.items {
if let ImplItem::Fn(fn_item) = item {
match fn_item.sig.ident.to_string().as_str() {
"get_prompt" => {
*item = get_prompt_impl.clone();
has_get_prompt = true;
}
"list_prompts" => {
*item = list_prompts_impl.clone();
has_list_prompts = true;
}
_ => {}
}
}
}
// Add methods if they don't exist
if !has_get_prompt {
impl_block.items.push(get_prompt_impl);
}
if !has_list_prompts {
impl_block.items.push(list_prompts_impl);
}
Ok(quote! {
#impl_block
})
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_prompt_handler_macro() -> syn::Result<()> {
let input = quote! {
impl ServerHandler for MyPromptHandler {
// Other handler methods...
}
};
let result = prompt_handler(TokenStream::new(), input)?;
let result_str = result.to_string();
// Check that the required methods were generated
assert!(result_str.contains("async fn get_prompt"));
assert!(result_str.contains("PromptContext") && result_str.contains("new"));
assert!(result_str.contains("async fn list_prompts"));
assert!(result_str.contains("ListPromptsResult"));
Ok(())
}
#[test]
fn test_prompt_handler_with_custom_router() -> syn::Result<()> {
let attr = quote! { router = self.get_prompt_router() };
let input = quote! {
impl ServerHandler for MyPromptHandler {
// Other handler methods...
}
};
let result = prompt_handler(attr, input)?;
let result_str = result.to_string();
// Check that the custom router expression is used
assert!(
result_str.contains("self")
&& result_str.contains("get_prompt_router")
&& result_str.contains("get_prompt")
);
assert!(
result_str.contains("self")
&& result_str.contains("get_prompt_router")
&& result_str.contains("list_all")
);
Ok(())
}
}

View file

@ -0,0 +1,111 @@
use darling::FromMeta;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{ImplItem, ItemImpl, Visibility, parse_quote};
#[derive(FromMeta, Debug, Default)]
#[darling(default)]
pub struct PromptRouterAttribute {
pub router: Option<String>,
pub vis: Option<Visibility>,
}
pub fn prompt_router(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
let attribute = if attr.is_empty() {
Default::default()
} else {
let attr_args = darling::ast::NestedMeta::parse_meta_list(attr)?;
PromptRouterAttribute::from_list(&attr_args)?
};
let mut impl_block = syn::parse2::<ItemImpl>(input)?;
let self_ty = &impl_block.self_ty;
let router_fn_ident = attribute
.router
.map(|s| format_ident!("{}", s))
.unwrap_or_else(|| format_ident!("prompt_router"));
let vis = attribute.vis.unwrap_or(Visibility::Inherited);
let mut prompt_route_fn_calls = Vec::new();
for item in &mut impl_block.items {
if let ImplItem::Fn(fn_item) = item {
let has_prompt_attr = fn_item.attrs.iter().any(|attr| {
attr.path()
.segments
.last()
.map(|seg| seg.ident == "prompt")
.unwrap_or(false)
});
if has_prompt_attr {
let fn_ident = &fn_item.sig.ident;
let attr_fn_ident = format_ident!("{}_prompt_attr", fn_ident);
// Check what parameters the function takes
let mut param_names = Vec::new();
let mut param_types = Vec::new();
for input in &fn_item.sig.inputs {
if let syn::FnArg::Typed(pat_type) = input {
// Extract parameter pattern and type
param_types.push(&*pat_type.ty);
param_names.push(&*pat_type.pat);
}
}
// Use the exact same pattern as tool_router
prompt_route_fn_calls.push(quote! {
.with_route((Self::#attr_fn_ident(), Self::#fn_ident))
});
}
}
}
let router_fn: ImplItem = parse_quote! {
#vis fn #router_fn_ident() -> rmcp::handler::server::router::prompt::PromptRouter<#self_ty> {
rmcp::handler::server::router::prompt::PromptRouter::new()
#(#prompt_route_fn_calls)*
}
};
impl_block.items.push(router_fn);
Ok(quote! {
#impl_block
})
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_prompt_router_macro() -> syn::Result<()> {
let input = quote! {
impl MyPromptHandler {
#[prompt]
async fn greeting_prompt(&self) -> Result<Vec<PromptMessage>, Error> {
Ok(vec![])
}
#[prompt]
async fn code_review_prompt(&self, Parameters(args): Parameters<CodeReviewArgs>) -> Result<Vec<PromptMessage>, Error> {
Ok(vec![])
}
}
};
let result = prompt_router(TokenStream::new(), input)?;
let result_str = result.to_string();
// Check that the prompt_router function was generated
assert!(result_str.contains("fn prompt_router"));
assert!(result_str.contains("PromptRouter :: new"));
assert!(result_str.contains("greeting_prompt_prompt_attr"));
assert!(result_str.contains("code_review_prompt_prompt_attr"));
Ok(())
}
}

View file

@ -2,6 +2,65 @@ use darling::{FromMeta, ast::NestedMeta};
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote};
use syn::{Expr, Ident, ImplItemFn, ReturnType};
use crate::common::{extract_doc_line, none_expr};
/// Check if a type is Json<T> and extract the inner type T
fn extract_json_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
if let syn::Type::Path(type_path) = ty {
if let Some(last_segment) = type_path.path.segments.last() {
if last_segment.ident == "Json" {
if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments {
if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first() {
return Some(inner_type);
}
}
}
}
}
None
}
/// Extract schema expression from a function's return type
/// Handles patterns like Json<T> and Result<Json<T>, E>
fn extract_schema_from_return_type(ret_type: &syn::Type) -> Option<Expr> {
// First, try direct Json<T>
if let Some(inner_type) = extract_json_inner_type(ret_type) {
return syn::parse2::<Expr>(quote! {
rmcp::handler::server::tool::cached_schema_for_type::<#inner_type>()
})
.ok();
}
// Then, try Result<Json<T>, E>
let type_path = match ret_type {
syn::Type::Path(path) => path,
_ => return None,
};
let last_segment = type_path.path.segments.last()?;
if last_segment.ident != "Result" {
return None;
}
let args = match &last_segment.arguments {
syn::PathArguments::AngleBracketed(args) => args,
_ => return None,
};
let ok_type = match args.args.first()? {
syn::GenericArgument::Type(ty) => ty,
_ => return None,
};
let inner_type = extract_json_inner_type(ok_type)?;
syn::parse2::<Expr>(quote! {
rmcp::handler::server::tool::cached_schema_for_type::<#inner_type>()
})
.ok()
}
#[derive(FromMeta, Default, Debug)]
#[darling(default)]
pub struct ToolAttribute {
@ -95,98 +154,6 @@ pub struct ToolAnnotationsAttribute {
pub open_world_hint: Option<bool>,
}
fn none_expr() -> Expr {
syn::parse2::<Expr>(quote! { None }).unwrap()
}
/// Check if a type is Json<T> and extract the inner type T
fn extract_json_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
if let syn::Type::Path(type_path) = ty {
if let Some(last_segment) = type_path.path.segments.last() {
if last_segment.ident == "Json" {
if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments {
if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first() {
return Some(inner_type);
}
}
}
}
}
None
}
/// Extract schema expression from a function's return type
/// Handles patterns like Json<T> and Result<Json<T>, E>
fn extract_schema_from_return_type(ret_type: &syn::Type) -> Option<Expr> {
// First, try direct Json<T>
if let Some(inner_type) = extract_json_inner_type(ret_type) {
return syn::parse2::<Expr>(quote! {
rmcp::handler::server::tool::cached_schema_for_type::<#inner_type>()
})
.ok();
}
// Then, try Result<Json<T>, E>
let type_path = match ret_type {
syn::Type::Path(path) => path,
_ => return None,
};
let last_segment = type_path.path.segments.last()?;
if last_segment.ident != "Result" {
return None;
}
let args = match &last_segment.arguments {
syn::PathArguments::AngleBracketed(args) => args,
_ => return None,
};
let ok_type = match args.args.first()? {
syn::GenericArgument::Type(ty) => ty,
_ => return None,
};
let inner_type = extract_json_inner_type(ok_type)?;
syn::parse2::<Expr>(quote! {
rmcp::handler::server::tool::cached_schema_for_type::<#inner_type>()
})
.ok()
}
// extract doc line from attribute
fn extract_doc_line(existing_docs: Option<String>, attr: &syn::Attribute) -> Option<String> {
if !attr.path().is_ident("doc") {
return None;
}
let syn::Meta::NameValue(name_value) = &attr.meta else {
return None;
};
let syn::Expr::Lit(expr_lit) = &name_value.value else {
return None;
};
let syn::Lit::Str(lit_str) = &expr_lit.lit else {
return None;
};
let content = lit_str.value().trim().to_string();
match (existing_docs, content) {
(Some(mut existing_docs), content) if !content.is_empty() => {
existing_docs.push('\n');
existing_docs.push_str(&content);
Some(existing_docs)
}
(Some(existing_docs), _) => Some(existing_docs),
(None, content) if !content.is_empty() => Some(content),
_ => None,
}
}
pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
let attribute = if attr.is_empty() {
Default::default()
@ -202,30 +169,16 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
input_schema
} else {
// try to find some parameters wrapper in the function
let params_ty = fn_item.sig.inputs.iter().find_map(|input| {
if let syn::FnArg::Typed(pat_type) = input {
if let syn::Type::Path(type_path) = &*pat_type.ty {
if type_path
.path
.segments
.last()
.is_some_and(|type_name| type_name.ident == "Parameters")
{
return Some(pat_type.ty.clone());
}
}
}
None
});
let params_ty = crate::common::find_parameters_type_impl(&fn_item);
if let Some(params_ty) = params_ty {
// if found, use the Parameters schema
syn::parse2::<Expr>(quote! {
rmcp::handler::server::tool::cached_schema_for_type::<#params_ty>()
rmcp::handler::server::common::cached_schema_for_type::<#params_ty>()
})?
} else {
// if not found, use the default EmptyObject schema
syn::parse2::<Expr>(quote! {
rmcp::handler::server::tool::cached_schema_for_type::<rmcp::model::EmptyObject>()
rmcp::handler::server::common::cached_schema_for_type::<rmcp::model::EmptyObject>()
})?
}
};
@ -257,7 +210,7 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
};
syn::parse2::<Expr>(token_stream)?
} else {
none_expr()
none_expr()?
};
// Handle output_schema - either explicit or generated from return type
let output_schema_expr = attribute.output_schema.or_else(|| {
@ -296,10 +249,10 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
}
match &fn_item.sig.output {
syn::ReturnType::Default => {
quote! { -> std::pin::Pin<Box<dyn Future<Output = ()> + Send + #lt>> }
quote! { -> futures::future::BoxFuture<#lt, ()> }
}
syn::ReturnType::Type(_, ty) => {
quote! { -> std::pin::Pin<Box<dyn Future<Output = #ty> + Send + #lt>> }
quote! { -> futures::future::BoxFuture<#lt, #ty> }
}
}
})?;