Skip to content

Commit be321a4

Browse files
authored
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
1 parent 5891b45 commit be321a4

File tree

25 files changed

+742
-233
lines changed

25 files changed

+742
-233
lines changed

README.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ For the full MCP specification, see [modelcontextprotocol.io](https://modelconte
2222
## Table of Contents
2323

2424
- [Usage](#usage)
25+
- [Tools](#tools)
2526
- [Resources](#resources)
2627
- [Prompts](#prompts)
2728
- [Sampling](#sampling)
@@ -129,6 +130,76 @@ let quit_reason = server.cancel().await?;
129130

130131
---
131132

133+
## Tools
134+
135+
Tools let servers expose callable functions to clients. Each tool has a name, description, and a JSON Schema for its parameters. Clients discover tools via `list_tools` and invoke them via `call_tool`.
136+
137+
**MCP Spec:** [Tools](https://modelcontextprotocol.io/specification/2025-11-25/server/tools)
138+
139+
### Server-side
140+
141+
The `#[tool]`, `#[tool_router]`, and `#[tool_handler]` macros handle all the wiring. For a tools-only server you can use `#[tool_router(server_handler)]` to skip the separate `ServerHandler` impl:
142+
143+
```rust,ignore
144+
use rmcp::{tool, tool_router, ServiceExt, transport::stdio};
145+
146+
#[derive(Clone)]
147+
struct Calculator;
148+
149+
#[tool_router(server_handler)]
150+
impl Calculator {
151+
#[tool(description = "Add two numbers")]
152+
fn add(&self, #[tool(param)] a: i32, #[tool(param)] b: i32) -> String {
153+
(a + b).to_string()
154+
}
155+
}
156+
157+
#[tokio::main]
158+
async fn main() -> anyhow::Result<()> {
159+
let service = Calculator.serve(stdio()).await?;
160+
service.waiting().await?;
161+
Ok(())
162+
}
163+
```
164+
165+
When you need custom server metadata or multiple capabilities (tools + prompts), use explicit `#[tool_handler]`:
166+
167+
```rust,ignore
168+
use rmcp::{tool, tool_router, tool_handler, ServerHandler, ServiceExt};
169+
170+
#[derive(Clone)]
171+
struct Calculator;
172+
173+
#[tool_router]
174+
impl Calculator {
175+
#[tool(description = "Add two numbers")]
176+
fn add(&self, #[tool(param)] a: i32, #[tool(param)] b: i32) -> String {
177+
(a + b).to_string()
178+
}
179+
}
180+
181+
#[tool_handler(name = "calculator", version = "1.0.0", instructions = "A simple calculator")]
182+
impl ServerHandler for Calculator {}
183+
```
184+
185+
See [`crates/rmcp-macros`](crates/rmcp-macros/README.md) for full macro documentation.
186+
187+
### Client-side
188+
189+
```rust,ignore
190+
use rmcp::model::CallToolRequestParams;
191+
192+
// List all tools
193+
let tools = client.list_all_tools().await?;
194+
195+
// Call a tool by name
196+
let result = client.call_tool(CallToolRequestParams::new("add")).await?;
197+
```
198+
199+
**Example:** [`examples/servers/src/common/calculator.rs`](examples/servers/src/common/calculator.rs) (server), [`examples/servers/src/calculator_stdio.rs`](examples/servers/src/calculator_stdio.rs) (stdio runner)
200+
201+
---
202+
132203
## Resources
133204

134205
Resources let servers expose data (files, database records, API responses) that clients can read. Each resource is identified by a URI and returns content as text or binary (base64-encoded) data. Resource templates allow servers to declare URI patterns with dynamic parameters.

crates/rmcp-macros/README.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ For **getting started** and **full MCP feature documentation**, see the [main RE
2020
| Macro | Description |
2121
|-------|-------------|
2222
| [`#[tool]`][tool] | Mark a function as an MCP tool handler |
23-
| [`#[tool_router]`][tool_router] | Generate a tool router from an impl block |
23+
| [`#[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) |
2424
| [`#[tool_handler]`][tool_handler] | Generate `call_tool` and `list_tools` handler methods |
2525
| [`#[prompt]`][prompt] | Mark a function as an MCP prompt handler |
2626
| [`#[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
3737

3838
## Quick Example
3939

40+
Tools-only server with a single `impl` block (`server_handler` expands `#[tool_handler]` in a second macro pass):
41+
4042
```rust,ignore
41-
use rmcp::{tool, tool_router, tool_handler, ServerHandler, model::*};
43+
use rmcp::{tool, tool_router};
4244
4345
#[derive(Clone)]
44-
struct MyServer {
45-
tool_router: rmcp::handler::server::tool::ToolRouter<Self>,
46+
struct MyServer;
47+
48+
#[tool_router(server_handler)]
49+
impl MyServer {
50+
#[tool(description = "Say hello")]
51+
async fn hello(&self) -> String {
52+
"Hello, world!".into()
53+
}
4654
}
55+
```
56+
57+
If you need custom `#[tool_handler(...)]` arguments (e.g. `instructions`, `name`, or stacked `#[prompt_handler]` on the same `impl ServerHandler`), use two blocks instead:
58+
59+
```rust,ignore
60+
use rmcp::{tool, tool_router, tool_handler, ServerHandler};
61+
62+
#[derive(Clone)]
63+
struct MyServer;
4764
4865
#[tool_router]
4966
impl MyServer {
@@ -54,11 +71,7 @@ impl MyServer {
5471
}
5572
5673
#[tool_handler]
57-
impl ServerHandler for MyServer {
58-
fn get_info(&self) -> ServerInfo {
59-
ServerInfo::default()
60-
}
61-
}
74+
impl ServerHandler for MyServer {}
6275
```
6376

6477
See the [full documentation](https://docs.rs/rmcp-macros) for detailed usage of each macro.

crates/rmcp-macros/src/common.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Common utilities shared between different macro implementations
22
33
use quote::quote;
4-
use syn::{Attribute, Expr, FnArg, ImplItemFn, Signature, Type};
4+
use syn::{Attribute, Expr, FnArg, ImplItem, ImplItemFn, ItemImpl, Signature, Type};
55

66
/// Parse a None expression
77
pub fn none_expr() -> syn::Result<Expr> {
@@ -75,3 +75,24 @@ pub fn find_parameters_type_in_sig(sig: &Signature) -> Option<Box<Type>> {
7575
pub fn find_parameters_type_impl(fn_item: &ImplItemFn) -> Option<Box<Type>> {
7676
find_parameters_type_in_sig(&fn_item.sig)
7777
}
78+
79+
/// Check whether an `impl` block already contains a method with the given name.
80+
pub fn has_method(name: &str, item_impl: &ItemImpl) -> bool {
81+
item_impl.items.iter().any(|item| match item {
82+
ImplItem::Fn(func) => func.sig.ident == name,
83+
_ => false,
84+
})
85+
}
86+
87+
/// Check whether an `impl` block carries a sibling handler attribute (e.g.
88+
/// `#[prompt_handler]` visible from within `#[tool_handler]`).
89+
///
90+
/// Matches both bare (`prompt_handler`) and path-qualified (`rmcp::prompt_handler`) forms.
91+
pub fn has_sibling_handler(item_impl: &ItemImpl, handler_name: &str) -> bool {
92+
item_impl.attrs.iter().any(|attr| {
93+
attr.path()
94+
.segments
95+
.last()
96+
.is_some_and(|seg| seg.ident == handler_name)
97+
})
98+
}

crates/rmcp-macros/src/lib.rs

Lines changed: 76 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,16 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> TokenStream {
4747
///
4848
/// It creates a function that returns a `ToolRouter` instance.
4949
///
50-
/// 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.
50+
/// The generated function is used by `#[tool_handler]` by default (via `Self::tool_router()`),
51+
/// so in most cases you do not need to store the router in a field.
52+
///
5153
/// ## Usage
5254
///
53-
/// | field | type | usage |
54-
/// | :- | :- | :- |
55-
/// | `router` | `Ident` | The name of the router function to be generated. Defaults to `tool_router`. |
56-
/// | `vis` | `Visibility` | The visibility of the generated router function. Defaults to empty. |
55+
/// | field | type | usage |
56+
/// | :- | :- | :- |
57+
/// | `router` | `Ident` | The name of the router function to be generated. Defaults to `tool_router`. |
58+
/// | `vis` | `Visibility` | The visibility of the generated router function. Defaults to empty. |
59+
/// | `server_handler` | `flag` | When set, also emits `#[::rmcp::tool_handler]` on `impl ServerHandler for Self` so you can omit a separate `#[tool_handler]` block. |
5760
///
5861
/// ## Example
5962
///
@@ -62,18 +65,33 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> TokenStream {
6265
/// impl MyToolHandler {
6366
/// #[tool]
6467
/// pub fn my_tool() {
65-
///
66-
/// }
6768
///
68-
/// pub fn new() -> Self {
69-
/// Self {
70-
/// // the default name of tool router will be `tool_router`
71-
/// tool_router: Self::tool_router(),
72-
/// }
7369
/// }
7470
/// }
71+
///
72+
/// // #[tool_handler] calls Self::tool_router() automatically
73+
/// #[tool_handler]
74+
/// impl ServerHandler for MyToolHandler {}
7575
/// ```
7676
///
77+
/// ### Eliding `#[tool_handler]`
78+
///
79+
/// For a tools-only server, pass `server_handler` so the `impl ServerHandler` block is not written by hand:
80+
///
81+
/// ```rust,ignore
82+
/// #[tool_router(server_handler)]
83+
/// impl MyToolHandler {
84+
/// #[tool]
85+
/// fn my_tool() {}
86+
/// }
87+
/// ```
88+
///
89+
/// This expands in two steps: first `#[tool_router]` emits the inherent impl plus
90+
/// `#[::rmcp::tool_handler] impl ServerHandler for MyToolHandler {}`, then `#[tool_handler]`
91+
/// fills in `call_tool`, `list_tools`, `get_info`, and related methods. If you combine tools with
92+
/// prompts or tasks on the **same** `impl ServerHandler` block (stacked `#[tool_handler]` /
93+
/// `#[prompt_handler]` attributes), keep using an explicit `#[tool_handler]` impl instead of `server_handler`.
94+
///
7795
/// Or specify the visibility and router name, which would be helpful when you want to combine multiple routers into one:
7896
///
7997
/// ```rust,ignore
@@ -114,50 +132,62 @@ pub fn tool_router(attr: TokenStream, input: TokenStream) -> TokenStream {
114132

115133
/// # tool_handler
116134
///
117-
/// This macro will generate the handler for `tool_call` and `list_tools` methods in the implementation block, by using an existing `ToolRouter` instance.
135+
/// This macro generates the `call_tool`, `list_tools`, `get_tool`, and (optionally)
136+
/// `get_info` methods for a `ServerHandler` implementation, using a `ToolRouter`.
118137
///
119138
/// ## Usage
120139
///
121-
/// | field | type | usage |
122-
/// | :- | :- | :- |
123-
/// | `router` | `Expr` | The expression to access the `ToolRouter` instance. Defaults to `self.tool_router`. |
124-
/// ## Example
140+
/// | field | type | usage |
141+
/// | :- | :- | :- |
142+
/// | `router` | `Expr` | The expression to access the `ToolRouter` instance. Defaults to `Self::tool_router()`. |
143+
/// | `meta` | `Expr` | Optional metadata for `ListToolsResult`. |
144+
/// | `name` | `String` | Custom server name. Defaults to `CARGO_CRATE_NAME`. |
145+
/// | `version` | `String` | Custom server version. Defaults to `CARGO_PKG_VERSION`. |
146+
/// | `instructions` | `String` | Optional human-readable instructions about using this server. |
147+
///
148+
/// ## Minimal example (no boilerplate)
149+
///
150+
/// The macro automatically generates `get_info()` with tools capability enabled
151+
/// and reads the server name/version from `Cargo.toml`:
152+
///
125153
/// ```rust,ignore
126-
/// #[tool_handler]
127-
/// impl ServerHandler for MyToolHandler {
128-
/// // ...implement other handler
154+
/// struct TimeServer;
155+
///
156+
/// #[tool_router]
157+
/// impl TimeServer {
158+
/// #[tool(description = "Get current time")]
159+
/// async fn get_time(&self) -> String { "12:00".into() }
129160
/// }
161+
///
162+
/// #[tool_handler]
163+
/// impl ServerHandler for TimeServer {}
130164
/// ```
131165
///
132-
/// or using a custom router expression:
166+
/// ## Custom server info
167+
///
168+
/// ```rust,ignore
169+
/// #[tool_handler(name = "my-server", version = "1.0.0", instructions = "A helpful server")]
170+
/// impl ServerHandler for MyToolHandler {}
171+
/// ```
172+
///
173+
/// ## Custom router expression
174+
///
133175
/// ```rust,ignore
134-
/// #[tool_handler(router = self.get_router().await)]
176+
/// #[tool_handler(router = self.tool_router)]
135177
/// impl ServerHandler for MyToolHandler {
136178
/// // ...implement other handler
137179
/// }
138180
/// ```
139181
///
140-
/// ## Explain
182+
/// ## Manual `get_info()`
183+
///
184+
/// If you provide your own `get_info()`, the macro will not generate one:
141185
///
142-
/// This macro will be expended to something like this:
143186
/// ```rust,ignore
187+
/// #[tool_handler]
144188
/// impl ServerHandler for MyToolHandler {
145-
/// async fn call_tool(
146-
/// &self,
147-
/// request: CallToolRequestParams,
148-
/// context: RequestContext<RoleServer>,
149-
/// ) -> Result<CallToolResult, rmcp::ErrorData> {
150-
/// let tcc = ToolCallContext::new(self, request, context);
151-
/// self.tool_router.call(tcc).await
152-
/// }
153-
///
154-
/// async fn list_tools(
155-
/// &self,
156-
/// _request: Option<PaginatedRequestParams>,
157-
/// _context: RequestContext<RoleServer>,
158-
/// ) -> Result<ListToolsResult, rmcp::ErrorData> {
159-
/// let items = self.tool_router.list_all();
160-
/// Ok(ListToolsResult::with_all_items(items))
189+
/// fn get_info(&self) -> ServerInfo {
190+
/// ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
161191
/// }
162192
/// }
163193
/// ```
@@ -237,13 +267,16 @@ pub fn prompt_router(attr: TokenStream, input: TokenStream) -> TokenStream {
237267

238268
/// # prompt_handler
239269
///
240-
/// This macro generates handler methods for `get_prompt` and `list_prompts` in the implementation block, using an existing `PromptRouter` instance.
270+
/// This macro generates handler methods for `get_prompt` and `list_prompts` in the
271+
/// implementation block, using a `PromptRouter`. It also auto-generates `get_info()`
272+
/// with prompts capability enabled if not already provided.
241273
///
242274
/// ## Usage
243275
///
244276
/// | field | type | usage |
245277
/// | :- | :- | :- |
246-
/// | `router` | `Expr` | The expression to access the `PromptRouter` instance. Defaults to `self.prompt_router`. |
278+
/// | `router` | `Expr` | The expression to access the `PromptRouter` instance. Defaults to `Self::prompt_router()`. |
279+
/// | `meta` | `Expr` | Optional metadata for `ListPromptsResult`. |
247280
///
248281
/// ## Example
249282
/// ```rust,ignore
@@ -255,7 +288,7 @@ pub fn prompt_router(attr: TokenStream, input: TokenStream) -> TokenStream {
255288
///
256289
/// or using a custom router expression:
257290
/// ```rust,ignore
258-
/// #[prompt_handler(router = self.get_prompt_router())]
291+
/// #[prompt_handler(router = self.prompt_router)]
259292
/// impl ServerHandler for MyPromptHandler {
260293
/// // ...implement other handler methods
261294
/// }

crates/rmcp-macros/src/prompt_handler.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ use proc_macro2::TokenStream;
33
use quote::quote;
44
use syn::{Expr, ImplItem, ItemImpl, parse_quote};
55

6+
use crate::{
7+
common::{has_method, has_sibling_handler},
8+
tool_handler::{CallerCapability, build_get_info},
9+
};
10+
611
#[derive(FromMeta, Debug, Default)]
712
#[darling(default)]
813
pub struct PromptHandlerAttribute {
@@ -22,7 +27,7 @@ pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> syn::Result<Toke
2227

2328
let router_expr = attribute
2429
.router
25-
.unwrap_or_else(|| syn::parse2(quote! { self.prompt_router }).unwrap());
30+
.unwrap_or_else(|| syn::parse2(quote! { Self::prompt_router() }).unwrap());
2631

2732
// Add get_prompt implementation
2833
let get_prompt_impl: ImplItem = parse_quote! {
@@ -91,6 +96,17 @@ pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> syn::Result<Toke
9196
impl_block.items.push(list_prompts_impl);
9297
}
9398

99+
// Auto-generate get_info() if not already provided
100+
if !has_method("get_info", &impl_block) {
101+
// Detect whether tool_handler is also present — if so, it will generate get_info
102+
// with both capabilities. Only generate here if tool_handler is NOT present.
103+
if !has_sibling_handler(&impl_block, "tool_handler") {
104+
let get_info_fn =
105+
build_get_info(&impl_block, None, None, None, CallerCapability::Prompts)?;
106+
impl_block.items.push(get_info_fn);
107+
}
108+
}
109+
94110
Ok(quote! {
95111
#impl_block
96112
})

0 commit comments

Comments
 (0)