feat(macros): auto-generate get_info and default router (#785)
* feat(macros): auto-generate get_info and default router * docs: simplify examples and docs with new defaults * feat(macros): add tool_router(server_handler) to elide separate #[tool_handler] impl * docs: add Tools section to README and simplify calculator examples with server_handler
This commit is contained in:
parent
5891b45162
commit
be321a4abe
7 changed files with 742 additions and 233 deletions
|
|
@ -20,7 +20,7 @@ For **getting started** and **full MCP feature documentation**, see the [main RE
|
|||
| Macro | Description |
|
||||
|-------|-------------|
|
||||
| [`#[tool]`][tool] | Mark a function as an MCP tool handler |
|
||||
| [`#[tool_router]`][tool_router] | Generate a tool router from an impl block |
|
||||
| [`#[tool_router]`][tool_router] | Generate a tool router from an impl block (optional `server_handler` flag elides a separate `#[tool_handler]` block for tools-only servers) |
|
||||
| [`#[tool_handler]`][tool_handler] | Generate `call_tool` and `list_tools` handler methods |
|
||||
| [`#[prompt]`][prompt] | Mark a function as an MCP prompt handler |
|
||||
| [`#[prompt_router]`][prompt_router] | Generate a prompt router from an impl block |
|
||||
|
|
@ -37,13 +37,30 @@ For **getting started** and **full MCP feature documentation**, see the [main RE
|
|||
|
||||
## Quick Example
|
||||
|
||||
Tools-only server with a single `impl` block (`server_handler` expands `#[tool_handler]` in a second macro pass):
|
||||
|
||||
```rust,ignore
|
||||
use rmcp::{tool, tool_router, tool_handler, ServerHandler, model::*};
|
||||
use rmcp::{tool, tool_router};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MyServer {
|
||||
tool_router: rmcp::handler::server::tool::ToolRouter<Self>,
|
||||
struct MyServer;
|
||||
|
||||
#[tool_router(server_handler)]
|
||||
impl MyServer {
|
||||
#[tool(description = "Say hello")]
|
||||
async fn hello(&self) -> String {
|
||||
"Hello, world!".into()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you need custom `#[tool_handler(...)]` arguments (e.g. `instructions`, `name`, or stacked `#[prompt_handler]` on the same `impl ServerHandler`), use two blocks instead:
|
||||
|
||||
```rust,ignore
|
||||
use rmcp::{tool, tool_router, tool_handler, ServerHandler};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MyServer;
|
||||
|
||||
#[tool_router]
|
||||
impl MyServer {
|
||||
|
|
@ -54,11 +71,7 @@ impl MyServer {
|
|||
}
|
||||
|
||||
#[tool_handler]
|
||||
impl ServerHandler for MyServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::default()
|
||||
}
|
||||
}
|
||||
impl ServerHandler for MyServer {}
|
||||
```
|
||||
|
||||
See the [full documentation](https://docs.rs/rmcp-macros) for detailed usage of each macro.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Common utilities shared between different macro implementations
|
||||
|
||||
use quote::quote;
|
||||
use syn::{Attribute, Expr, FnArg, ImplItemFn, Signature, Type};
|
||||
use syn::{Attribute, Expr, FnArg, ImplItem, ImplItemFn, ItemImpl, Signature, Type};
|
||||
|
||||
/// Parse a None expression
|
||||
pub fn none_expr() -> syn::Result<Expr> {
|
||||
|
|
@ -75,3 +75,24 @@ pub fn find_parameters_type_in_sig(sig: &Signature) -> Option<Box<Type>> {
|
|||
pub fn find_parameters_type_impl(fn_item: &ImplItemFn) -> Option<Box<Type>> {
|
||||
find_parameters_type_in_sig(&fn_item.sig)
|
||||
}
|
||||
|
||||
/// Check whether an `impl` block already contains a method with the given name.
|
||||
pub fn has_method(name: &str, item_impl: &ItemImpl) -> bool {
|
||||
item_impl.items.iter().any(|item| match item {
|
||||
ImplItem::Fn(func) => func.sig.ident == name,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check whether an `impl` block carries a sibling handler attribute (e.g.
|
||||
/// `#[prompt_handler]` visible from within `#[tool_handler]`).
|
||||
///
|
||||
/// Matches both bare (`prompt_handler`) and path-qualified (`rmcp::prompt_handler`) forms.
|
||||
pub fn has_sibling_handler(item_impl: &ItemImpl, handler_name: &str) -> bool {
|
||||
item_impl.attrs.iter().any(|attr| {
|
||||
attr.path()
|
||||
.segments
|
||||
.last()
|
||||
.is_some_and(|seg| seg.ident == handler_name)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,13 +47,16 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> TokenStream {
|
|||
///
|
||||
/// It creates a function that returns a `ToolRouter` instance.
|
||||
///
|
||||
/// In most case, you need to add a field for handler to store the router information and initialize it when creating handler, or store it with a static variable.
|
||||
/// The generated function is used by `#[tool_handler]` by default (via `Self::tool_router()`),
|
||||
/// so in most cases you do not need to store the router in a field.
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// | field | type | usage |
|
||||
/// | :- | :- | :- |
|
||||
/// | `router` | `Ident` | The name of the router function to be generated. Defaults to `tool_router`. |
|
||||
/// | `vis` | `Visibility` | The visibility of the generated router function. Defaults to empty. |
|
||||
/// | field | type | usage |
|
||||
/// | :- | :- | :- |
|
||||
/// | `router` | `Ident` | The name of the router function to be generated. Defaults to `tool_router`. |
|
||||
/// | `vis` | `Visibility` | The visibility of the generated router function. Defaults to empty. |
|
||||
/// | `server_handler` | `flag` | When set, also emits `#[::rmcp::tool_handler]` on `impl ServerHandler for Self` so you can omit a separate `#[tool_handler]` block. |
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
|
|
@ -62,18 +65,33 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> TokenStream {
|
|||
/// impl MyToolHandler {
|
||||
/// #[tool]
|
||||
/// pub fn my_tool() {
|
||||
///
|
||||
/// }
|
||||
///
|
||||
/// pub fn new() -> Self {
|
||||
/// Self {
|
||||
/// // the default name of tool router will be `tool_router`
|
||||
/// tool_router: Self::tool_router(),
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // #[tool_handler] calls Self::tool_router() automatically
|
||||
/// #[tool_handler]
|
||||
/// impl ServerHandler for MyToolHandler {}
|
||||
/// ```
|
||||
///
|
||||
/// ### Eliding `#[tool_handler]`
|
||||
///
|
||||
/// For a tools-only server, pass `server_handler` so the `impl ServerHandler` block is not written by hand:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[tool_router(server_handler)]
|
||||
/// impl MyToolHandler {
|
||||
/// #[tool]
|
||||
/// fn my_tool() {}
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This expands in two steps: first `#[tool_router]` emits the inherent impl plus
|
||||
/// `#[::rmcp::tool_handler] impl ServerHandler for MyToolHandler {}`, then `#[tool_handler]`
|
||||
/// fills in `call_tool`, `list_tools`, `get_info`, and related methods. If you combine tools with
|
||||
/// prompts or tasks on the **same** `impl ServerHandler` block (stacked `#[tool_handler]` /
|
||||
/// `#[prompt_handler]` attributes), keep using an explicit `#[tool_handler]` impl instead of `server_handler`.
|
||||
///
|
||||
/// Or specify the visibility and router name, which would be helpful when you want to combine multiple routers into one:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
|
|
@ -114,50 +132,62 @@ pub fn tool_router(attr: TokenStream, input: TokenStream) -> TokenStream {
|
|||
|
||||
/// # tool_handler
|
||||
///
|
||||
/// This macro will generate the handler for `tool_call` and `list_tools` methods in the implementation block, by using an existing `ToolRouter` instance.
|
||||
/// This macro generates the `call_tool`, `list_tools`, `get_tool`, and (optionally)
|
||||
/// `get_info` methods for a `ServerHandler` implementation, using a `ToolRouter`.
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// | field | type | usage |
|
||||
/// | :- | :- | :- |
|
||||
/// | `router` | `Expr` | The expression to access the `ToolRouter` instance. Defaults to `self.tool_router`. |
|
||||
/// ## Example
|
||||
/// | field | type | usage |
|
||||
/// | :- | :- | :- |
|
||||
/// | `router` | `Expr` | The expression to access the `ToolRouter` instance. Defaults to `Self::tool_router()`. |
|
||||
/// | `meta` | `Expr` | Optional metadata for `ListToolsResult`. |
|
||||
/// | `name` | `String` | Custom server name. Defaults to `CARGO_CRATE_NAME`. |
|
||||
/// | `version` | `String` | Custom server version. Defaults to `CARGO_PKG_VERSION`. |
|
||||
/// | `instructions` | `String` | Optional human-readable instructions about using this server. |
|
||||
///
|
||||
/// ## Minimal example (no boilerplate)
|
||||
///
|
||||
/// The macro automatically generates `get_info()` with tools capability enabled
|
||||
/// and reads the server name/version from `Cargo.toml`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[tool_handler]
|
||||
/// impl ServerHandler for MyToolHandler {
|
||||
/// // ...implement other handler
|
||||
/// struct TimeServer;
|
||||
///
|
||||
/// #[tool_router]
|
||||
/// impl TimeServer {
|
||||
/// #[tool(description = "Get current time")]
|
||||
/// async fn get_time(&self) -> String { "12:00".into() }
|
||||
/// }
|
||||
///
|
||||
/// #[tool_handler]
|
||||
/// impl ServerHandler for TimeServer {}
|
||||
/// ```
|
||||
///
|
||||
/// or using a custom router expression:
|
||||
/// ## Custom server info
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[tool_handler(router = self.get_router().await)]
|
||||
/// #[tool_handler(name = "my-server", version = "1.0.0", instructions = "A helpful server")]
|
||||
/// impl ServerHandler for MyToolHandler {}
|
||||
/// ```
|
||||
///
|
||||
/// ## Custom router expression
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[tool_handler(router = self.tool_router)]
|
||||
/// impl ServerHandler for MyToolHandler {
|
||||
/// // ...implement other handler
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Explain
|
||||
/// ## Manual `get_info()`
|
||||
///
|
||||
/// If you provide your own `get_info()`, the macro will not generate one:
|
||||
///
|
||||
/// This macro will be expended to something like this:
|
||||
/// ```rust,ignore
|
||||
/// #[tool_handler]
|
||||
/// impl ServerHandler for MyToolHandler {
|
||||
/// async fn call_tool(
|
||||
/// &self,
|
||||
/// request: CallToolRequestParams,
|
||||
/// context: RequestContext<RoleServer>,
|
||||
/// ) -> Result<CallToolResult, rmcp::ErrorData> {
|
||||
/// let tcc = ToolCallContext::new(self, request, context);
|
||||
/// self.tool_router.call(tcc).await
|
||||
/// }
|
||||
///
|
||||
/// async fn list_tools(
|
||||
/// &self,
|
||||
/// _request: Option<PaginatedRequestParams>,
|
||||
/// _context: RequestContext<RoleServer>,
|
||||
/// ) -> Result<ListToolsResult, rmcp::ErrorData> {
|
||||
/// let items = self.tool_router.list_all();
|
||||
/// Ok(ListToolsResult::with_all_items(items))
|
||||
/// fn get_info(&self) -> ServerInfo {
|
||||
/// ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
|
|
@ -237,13 +267,16 @@ pub fn prompt_router(attr: TokenStream, input: TokenStream) -> TokenStream {
|
|||
|
||||
/// # prompt_handler
|
||||
///
|
||||
/// This macro generates handler methods for `get_prompt` and `list_prompts` in the implementation block, using an existing `PromptRouter` instance.
|
||||
/// This macro generates handler methods for `get_prompt` and `list_prompts` in the
|
||||
/// implementation block, using a `PromptRouter`. It also auto-generates `get_info()`
|
||||
/// with prompts capability enabled if not already provided.
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// | field | type | usage |
|
||||
/// | :- | :- | :- |
|
||||
/// | `router` | `Expr` | The expression to access the `PromptRouter` instance. Defaults to `self.prompt_router`. |
|
||||
/// | `router` | `Expr` | The expression to access the `PromptRouter` instance. Defaults to `Self::prompt_router()`. |
|
||||
/// | `meta` | `Expr` | Optional metadata for `ListPromptsResult`. |
|
||||
///
|
||||
/// ## Example
|
||||
/// ```rust,ignore
|
||||
|
|
@ -255,7 +288,7 @@ pub fn prompt_router(attr: TokenStream, input: TokenStream) -> TokenStream {
|
|||
///
|
||||
/// or using a custom router expression:
|
||||
/// ```rust,ignore
|
||||
/// #[prompt_handler(router = self.get_prompt_router())]
|
||||
/// #[prompt_handler(router = self.prompt_router)]
|
||||
/// impl ServerHandler for MyPromptHandler {
|
||||
/// // ...implement other handler methods
|
||||
/// }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@ use proc_macro2::TokenStream;
|
|||
use quote::quote;
|
||||
use syn::{Expr, ImplItem, ItemImpl, parse_quote};
|
||||
|
||||
use crate::{
|
||||
common::{has_method, has_sibling_handler},
|
||||
tool_handler::{CallerCapability, build_get_info},
|
||||
};
|
||||
|
||||
#[derive(FromMeta, Debug, Default)]
|
||||
#[darling(default)]
|
||||
pub struct PromptHandlerAttribute {
|
||||
|
|
@ -22,7 +27,7 @@ pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> syn::Result<Toke
|
|||
|
||||
let router_expr = attribute
|
||||
.router
|
||||
.unwrap_or_else(|| syn::parse2(quote! { self.prompt_router }).unwrap());
|
||||
.unwrap_or_else(|| syn::parse2(quote! { Self::prompt_router() }).unwrap());
|
||||
|
||||
// Add get_prompt implementation
|
||||
let get_prompt_impl: ImplItem = parse_quote! {
|
||||
|
|
@ -91,6 +96,17 @@ pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> syn::Result<Toke
|
|||
impl_block.items.push(list_prompts_impl);
|
||||
}
|
||||
|
||||
// Auto-generate get_info() if not already provided
|
||||
if !has_method("get_info", &impl_block) {
|
||||
// Detect whether tool_handler is also present — if so, it will generate get_info
|
||||
// with both capabilities. Only generate here if tool_handler is NOT present.
|
||||
if !has_sibling_handler(&impl_block, "tool_handler") {
|
||||
let get_info_fn =
|
||||
build_get_info(&impl_block, None, None, None, CallerCapability::Prompts)?;
|
||||
impl_block.items.push(get_info_fn);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(quote! {
|
||||
#impl_block
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ use proc_macro2::TokenStream;
|
|||
use quote::{ToTokens, quote};
|
||||
use syn::{Expr, ImplItem, ItemImpl};
|
||||
|
||||
use crate::common::{has_method, has_sibling_handler};
|
||||
|
||||
#[derive(FromMeta)]
|
||||
#[darling(default)]
|
||||
struct TaskHandlerAttribute {
|
||||
|
|
@ -20,14 +22,7 @@ impl Default for TaskHandlerAttribute {
|
|||
pub fn task_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
|
||||
let attr_args = NestedMeta::parse_meta_list(attr)?;
|
||||
let TaskHandlerAttribute { processor } = TaskHandlerAttribute::from_list(&attr_args)?;
|
||||
let mut item_impl = syn::parse2::<ItemImpl>(input.clone())?;
|
||||
|
||||
let has_method = |name: &str, item_impl: &ItemImpl| -> bool {
|
||||
item_impl.items.iter().any(|item| match item {
|
||||
ImplItem::Fn(func) => func.sig.ident == name,
|
||||
_ => false,
|
||||
})
|
||||
};
|
||||
let mut item_impl = syn::parse2::<ItemImpl>(input)?;
|
||||
|
||||
if !has_method("list_tasks", &item_impl) {
|
||||
let list_fn = quote! {
|
||||
|
|
@ -262,5 +257,21 @@ pub fn task_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS
|
|||
item_impl.items.push(syn::parse2::<ImplItem>(cancel_fn)?);
|
||||
}
|
||||
|
||||
// Auto-generate get_info() if not already provided and no sibling tool/prompt handler
|
||||
// will generate it (they take priority since they run as outer attributes).
|
||||
if !has_method("get_info", &item_impl)
|
||||
&& !has_sibling_handler(&item_impl, "tool_handler")
|
||||
&& !has_sibling_handler(&item_impl, "prompt_handler")
|
||||
{
|
||||
let get_info_fn = crate::tool_handler::build_get_info(
|
||||
&item_impl,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
crate::tool_handler::CallerCapability::Tasks,
|
||||
)?;
|
||||
item_impl.items.push(get_info_fn);
|
||||
}
|
||||
|
||||
Ok(item_impl.into_token_stream())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,39 +3,57 @@ use proc_macro2::TokenStream;
|
|||
use quote::{ToTokens, quote};
|
||||
use syn::{Expr, ImplItem, ItemImpl};
|
||||
|
||||
use crate::common::{has_method, has_sibling_handler};
|
||||
|
||||
#[derive(FromMeta)]
|
||||
#[darling(default)]
|
||||
pub struct ToolHandlerAttribute {
|
||||
pub router: Expr,
|
||||
pub meta: Option<Expr>,
|
||||
pub name: Option<String>,
|
||||
pub version: Option<String>,
|
||||
pub instructions: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ToolHandlerAttribute {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
router: syn::parse2(quote! {
|
||||
self.tool_router
|
||||
Self::tool_router()
|
||||
})
|
||||
.unwrap(),
|
||||
meta: None,
|
||||
name: None,
|
||||
version: None,
|
||||
instructions: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tool_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
|
||||
let attr_args = NestedMeta::parse_meta_list(attr)?;
|
||||
let ToolHandlerAttribute { router, meta } = ToolHandlerAttribute::from_list(&attr_args)?;
|
||||
let mut item_impl = syn::parse2::<ItemImpl>(input.clone())?;
|
||||
let tool_call_fn = quote! {
|
||||
async fn call_tool(
|
||||
&self,
|
||||
request: rmcp::model::CallToolRequestParams,
|
||||
context: rmcp::service::RequestContext<rmcp::RoleServer>,
|
||||
) -> Result<rmcp::model::CallToolResult, rmcp::ErrorData> {
|
||||
let tcc = rmcp::handler::server::tool::ToolCallContext::new(self, request, context);
|
||||
#router.call(tcc).await
|
||||
}
|
||||
};
|
||||
let ToolHandlerAttribute {
|
||||
router,
|
||||
meta,
|
||||
name,
|
||||
version,
|
||||
instructions,
|
||||
} = ToolHandlerAttribute::from_list(&attr_args)?;
|
||||
let mut item_impl = syn::parse2::<ItemImpl>(input)?;
|
||||
|
||||
if !has_method("call_tool", &item_impl) {
|
||||
let tool_call_fn = syn::parse2::<ImplItem>(quote! {
|
||||
async fn call_tool(
|
||||
&self,
|
||||
request: rmcp::model::CallToolRequestParams,
|
||||
context: rmcp::service::RequestContext<rmcp::RoleServer>,
|
||||
) -> Result<rmcp::model::CallToolResult, rmcp::ErrorData> {
|
||||
let tcc = rmcp::handler::server::tool::ToolCallContext::new(self, request, context);
|
||||
#router.call(tcc).await
|
||||
}
|
||||
})?;
|
||||
item_impl.items.push(tool_call_fn);
|
||||
}
|
||||
|
||||
let result_meta = if let Some(meta) = meta {
|
||||
quote! { Some(#meta) }
|
||||
|
|
@ -43,31 +61,109 @@ pub fn tool_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS
|
|||
quote! { None }
|
||||
};
|
||||
|
||||
let tool_list_fn = quote! {
|
||||
async fn list_tools(
|
||||
&self,
|
||||
_request: Option<rmcp::model::PaginatedRequestParams>,
|
||||
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
|
||||
) -> Result<rmcp::model::ListToolsResult, rmcp::ErrorData> {
|
||||
Ok(rmcp::model::ListToolsResult{
|
||||
tools: #router.list_all(),
|
||||
meta: #result_meta,
|
||||
next_cursor: None,
|
||||
})
|
||||
}
|
||||
};
|
||||
if !has_method("list_tools", &item_impl) {
|
||||
let tool_list_fn = syn::parse2::<ImplItem>(quote! {
|
||||
async fn list_tools(
|
||||
&self,
|
||||
_request: Option<rmcp::model::PaginatedRequestParams>,
|
||||
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
|
||||
) -> Result<rmcp::model::ListToolsResult, rmcp::ErrorData> {
|
||||
Ok(rmcp::model::ListToolsResult{
|
||||
tools: #router.list_all(),
|
||||
meta: #result_meta,
|
||||
next_cursor: None,
|
||||
})
|
||||
}
|
||||
})?;
|
||||
item_impl.items.push(tool_list_fn);
|
||||
}
|
||||
|
||||
let get_tool_fn = quote! {
|
||||
fn get_tool(&self, name: &str) -> Option<rmcp::model::Tool> {
|
||||
#router.get(name).cloned()
|
||||
}
|
||||
};
|
||||
if !has_method("get_tool", &item_impl) {
|
||||
let get_tool_fn = syn::parse2::<ImplItem>(quote! {
|
||||
fn get_tool(&self, name: &str) -> Option<rmcp::model::Tool> {
|
||||
#router.get(name).cloned()
|
||||
}
|
||||
})?;
|
||||
item_impl.items.push(get_tool_fn);
|
||||
}
|
||||
|
||||
// Auto-generate get_info() if not already provided
|
||||
if !has_method("get_info", &item_impl) {
|
||||
let get_info_fn = build_get_info(
|
||||
&item_impl,
|
||||
name,
|
||||
version,
|
||||
instructions,
|
||||
CallerCapability::Tools,
|
||||
)?;
|
||||
item_impl.items.push(get_info_fn);
|
||||
}
|
||||
|
||||
let tool_call_fn = syn::parse2::<ImplItem>(tool_call_fn)?;
|
||||
let tool_list_fn = syn::parse2::<ImplItem>(tool_list_fn)?;
|
||||
let get_tool_fn = syn::parse2::<ImplItem>(get_tool_fn)?;
|
||||
item_impl.items.push(tool_call_fn);
|
||||
item_impl.items.push(tool_list_fn);
|
||||
item_impl.items.push(get_tool_fn);
|
||||
Ok(item_impl.into_token_stream())
|
||||
}
|
||||
|
||||
/// Which handler macro is generating `get_info()`.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum CallerCapability {
|
||||
Tools,
|
||||
Prompts,
|
||||
Tasks,
|
||||
}
|
||||
|
||||
/// Build a `get_info()` method that returns `ServerInfo` with the appropriate capabilities.
|
||||
///
|
||||
/// The caller declares its own capability via `caller`. Sibling handler attributes
|
||||
/// (`prompt_handler`, `task_handler`, `tool_handler`) are detected automatically
|
||||
/// and their capabilities are included.
|
||||
pub(crate) fn build_get_info(
|
||||
item_impl: &ItemImpl,
|
||||
name: Option<String>,
|
||||
version: Option<String>,
|
||||
instructions: Option<String>,
|
||||
caller: CallerCapability,
|
||||
) -> syn::Result<ImplItem> {
|
||||
let has_tools =
|
||||
caller == CallerCapability::Tools || has_sibling_handler(item_impl, "tool_handler");
|
||||
let has_prompts =
|
||||
caller == CallerCapability::Prompts || has_sibling_handler(item_impl, "prompt_handler");
|
||||
let has_tasks =
|
||||
caller == CallerCapability::Tasks || has_sibling_handler(item_impl, "task_handler");
|
||||
|
||||
let mut capability_calls = Vec::new();
|
||||
if has_tools {
|
||||
capability_calls.push(quote! { .enable_tools() });
|
||||
}
|
||||
if has_prompts {
|
||||
capability_calls.push(quote! { .enable_prompts() });
|
||||
}
|
||||
if has_tasks {
|
||||
capability_calls.push(quote! { .enable_tasks() });
|
||||
}
|
||||
|
||||
let server_info_expr = match (name, version) {
|
||||
(Some(n), Some(v)) => quote! { rmcp::model::Implementation::new(#n, #v) },
|
||||
(Some(n), None) => {
|
||||
quote! { rmcp::model::Implementation::new(#n, env!("CARGO_PKG_VERSION")) }
|
||||
}
|
||||
(None, Some(v)) => {
|
||||
quote! { rmcp::model::Implementation::new(env!("CARGO_CRATE_NAME"), #v) }
|
||||
}
|
||||
(None, None) => quote! { rmcp::model::Implementation::from_build_env() },
|
||||
};
|
||||
|
||||
let mut builder_calls = vec![quote! { .with_server_info(#server_info_expr) }];
|
||||
if let Some(i) = instructions {
|
||||
builder_calls.push(quote! { .with_instructions(#i.to_string()) });
|
||||
}
|
||||
|
||||
syn::parse2::<ImplItem>(quote! {
|
||||
fn get_info(&self) -> rmcp::model::ServerInfo {
|
||||
rmcp::model::ServerInfo::new(
|
||||
rmcp::model::ServerCapabilities::builder()
|
||||
#(#capability_calls)*
|
||||
.build()
|
||||
)
|
||||
#(#builder_calls)*
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
//! ```ignore
|
||||
//! #[rmcp::tool_router(router)]
|
||||
//! impl Handler {
|
||||
//!
|
||||
//! }
|
||||
//! ```
|
||||
//! Procedural macro implementation for `#[tool_router]` (see `lib.rs`).
|
||||
//!
|
||||
//! When `server_handler` is set, we emit a second `impl ServerHandler` item decorated with
|
||||
//! `#[::rmcp::tool_handler]` so `tool_handler` expands in a later proc-macro pass—keeping all
|
||||
//! tool dispatch and `get_info` logic in `tool_handler.rs` without duplicating it here.
|
||||
|
||||
use darling::{FromMeta, ast::NestedMeta};
|
||||
use proc_macro2::TokenStream;
|
||||
|
|
@ -16,6 +14,9 @@ use syn::{Ident, ImplItem, ItemImpl, Visibility};
|
|||
pub struct ToolRouterAttribute {
|
||||
pub router: Ident,
|
||||
pub vis: Option<Visibility>,
|
||||
/// When set, also emit `#[::rmcp::tool_handler]` on `impl ServerHandler for Self` so callers
|
||||
/// can skip a separate `#[tool_handler]` block (expanded in a later macro pass).
|
||||
pub server_handler: bool,
|
||||
}
|
||||
|
||||
impl Default for ToolRouterAttribute {
|
||||
|
|
@ -23,14 +24,19 @@ impl Default for ToolRouterAttribute {
|
|||
Self {
|
||||
router: format_ident!("tool_router"),
|
||||
vis: None,
|
||||
server_handler: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tool_router(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
|
||||
let attr_args = NestedMeta::parse_meta_list(attr)?;
|
||||
let ToolRouterAttribute { router, vis } = ToolRouterAttribute::from_list(&attr_args)?;
|
||||
let mut item_impl = syn::parse2::<ItemImpl>(input.clone())?;
|
||||
let ToolRouterAttribute {
|
||||
router,
|
||||
vis,
|
||||
server_handler,
|
||||
} = ToolRouterAttribute::from_list(&attr_args)?;
|
||||
let mut item_impl = syn::parse2::<ItemImpl>(input)?;
|
||||
// find all function marked with `#[rmcp::tool]`
|
||||
let tool_attr_fns: Vec<_> = item_impl
|
||||
.items
|
||||
|
|
@ -52,7 +58,7 @@ pub fn tool_router(attr: TokenStream, input: TokenStream) -> syn::Result<TokenSt
|
|||
}
|
||||
})
|
||||
.collect();
|
||||
let mut routers = vec![];
|
||||
let mut routers = Vec::with_capacity(tool_attr_fns.len());
|
||||
for handler in tool_attr_fns {
|
||||
let tool_attr_fn_ident = format_ident!("{handler}_tool_attr");
|
||||
routers.push(quote! {
|
||||
|
|
@ -66,26 +72,69 @@ pub fn tool_router(attr: TokenStream, input: TokenStream) -> syn::Result<TokenSt
|
|||
}
|
||||
})?;
|
||||
item_impl.items.push(router_fn);
|
||||
Ok(item_impl.into_token_stream())
|
||||
|
||||
if !server_handler {
|
||||
return Ok(item_impl.into_token_stream());
|
||||
}
|
||||
|
||||
if item_impl.trait_.is_some() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
item_impl,
|
||||
"`server_handler` is only supported on inherent impl blocks (e.g. `impl MyType { ... }`)",
|
||||
));
|
||||
}
|
||||
|
||||
let self_ty = &item_impl.self_ty;
|
||||
let (impl_generics, ty_generics, where_clause) = item_impl.generics.split_for_impl();
|
||||
|
||||
Ok(quote! {
|
||||
#item_impl
|
||||
|
||||
#[::rmcp::tool_handler(router = Self::#router())]
|
||||
impl #impl_generics ::rmcp::ServerHandler for #self_ty #ty_generics #where_clause {}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_router_attr() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn tool_router_attribute_parses_router_visibility_and_defaults_server_handler_off()
|
||||
-> syn::Result<()> {
|
||||
let attr = quote! {
|
||||
router = test_router,
|
||||
vis = "pub(crate)"
|
||||
};
|
||||
let attr_args = NestedMeta::parse_meta_list(attr)?;
|
||||
let ToolRouterAttribute { router, vis } = ToolRouterAttribute::from_list(&attr_args)?;
|
||||
println!("router: {}", router);
|
||||
if let Some(vis) = vis {
|
||||
println!("visibility: {}", vis.to_token_stream());
|
||||
} else {
|
||||
println!("visibility: None");
|
||||
}
|
||||
let ToolRouterAttribute {
|
||||
router,
|
||||
vis,
|
||||
server_handler,
|
||||
} = ToolRouterAttribute::from_list(&attr_args)?;
|
||||
assert_eq!(router.to_string(), "test_router");
|
||||
assert!(vis.is_some(), "vis = \"pub(crate)\" should parse");
|
||||
assert!(
|
||||
!server_handler,
|
||||
"server_handler should default to false when omitted"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_router_attribute_parses_server_handler_flag() -> syn::Result<()> {
|
||||
let attr = quote! {
|
||||
router = custom_router,
|
||||
server_handler
|
||||
};
|
||||
let attr_args = NestedMeta::parse_meta_list(attr)?;
|
||||
let ToolRouterAttribute {
|
||||
router,
|
||||
server_handler,
|
||||
..
|
||||
} = ToolRouterAttribute::from_list(&attr_args)?;
|
||||
assert_eq!(router.to_string(), "custom_router");
|
||||
assert!(server_handler);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue