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:
Dale Seo 2026-04-08 15:06:26 -04:00 • committed by GitHub
commit be321a4abe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 742 additions and 233 deletions

View file

@ -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.

View file

@ -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)
})
}

View file

@ -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
/// }

View file

@ -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
})

View file

@ -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())
}

View file

@ -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)*
}
})
}

View file

@ -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(())
}
}