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:
parent
c0452f93a4
commit
9349f5cb26
1 changed files with 3191 additions and 464 deletions
|
|
@ -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> }
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
|
|
|||
Loading…
Reference in a new issue