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
parent 5891b45162
commit be321a4abe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 743 additions and 234 deletions

View file

@ -22,6 +22,7 @@ For the full MCP specification, see [modelcontextprotocol.io](https://modelconte
## Table of Contents ## Table of Contents
- [Usage](#usage) - [Usage](#usage)
- [Tools](#tools)
- [Resources](#resources) - [Resources](#resources)
- [Prompts](#prompts) - [Prompts](#prompts)
- [Sampling](#sampling) - [Sampling](#sampling)
@ -129,6 +130,76 @@ let quit_reason = server.cancel().await?;
--- ---
## Tools
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`.
**MCP Spec:** [Tools](https://modelcontextprotocol.io/specification/2025-11-25/server/tools)
### Server-side
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:
```rust,ignore
use rmcp::{tool, tool_router, ServiceExt, transport::stdio};
#[derive(Clone)]
struct Calculator;
#[tool_router(server_handler)]
impl Calculator {
#[tool(description = "Add two numbers")]
fn add(&self, #[tool(param)] a: i32, #[tool(param)] b: i32) -> String {
(a + b).to_string()
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let service = Calculator.serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
```
When you need custom server metadata or multiple capabilities (tools + prompts), use explicit `#[tool_handler]`:
```rust,ignore
use rmcp::{tool, tool_router, tool_handler, ServerHandler, ServiceExt};
#[derive(Clone)]
struct Calculator;
#[tool_router]
impl Calculator {
#[tool(description = "Add two numbers")]
fn add(&self, #[tool(param)] a: i32, #[tool(param)] b: i32) -> String {
(a + b).to_string()
}
}
#[tool_handler(name = "calculator", version = "1.0.0", instructions = "A simple calculator")]
impl ServerHandler for Calculator {}
```
See [`crates/rmcp-macros`](crates/rmcp-macros/README.md) for full macro documentation.
### Client-side
```rust,ignore
use rmcp::model::CallToolRequestParams;
// List all tools
let tools = client.list_all_tools().await?;
// Call a tool by name
let result = client.call_tool(CallToolRequestParams::new("add")).await?;
```
**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)
---
## Resources ## Resources
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. 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.

View file

@ -20,7 +20,7 @@ For **getting started** and **full MCP feature documentation**, see the [main RE
| Macro | Description | | Macro | Description |
|-------|-------------| |-------|-------------|
| [`#[tool]`][tool] | Mark a function as an MCP tool handler | | [`#[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 | | [`#[tool_handler]`][tool_handler] | Generate `call_tool` and `list_tools` handler methods |
| [`#[prompt]`][prompt] | Mark a function as an MCP prompt handler | | [`#[prompt]`][prompt] | Mark a function as an MCP prompt handler |
| [`#[prompt_router]`][prompt_router] | Generate a prompt router from an impl block | | [`#[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 ## Quick Example
Tools-only server with a single `impl` block (`server_handler` expands `#[tool_handler]` in a second macro pass):
```rust,ignore ```rust,ignore
use rmcp::{tool, tool_router, tool_handler, ServerHandler, model::*}; use rmcp::{tool, tool_router};
#[derive(Clone)] #[derive(Clone)]
struct MyServer { struct MyServer;
tool_router: rmcp::handler::server::tool::ToolRouter<Self>,
#[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] #[tool_router]
impl MyServer { impl MyServer {
@ -54,11 +71,7 @@ impl MyServer {
} }
#[tool_handler] #[tool_handler]
impl ServerHandler for MyServer { impl ServerHandler for MyServer {}
fn get_info(&self) -> ServerInfo {
ServerInfo::default()
}
}
``` ```
See the [full documentation](https://docs.rs/rmcp-macros) for detailed usage of each macro. 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 //! Common utilities shared between different macro implementations
use quote::quote; 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 /// Parse a None expression
pub fn none_expr() -> syn::Result<Expr> { 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>> { pub fn find_parameters_type_impl(fn_item: &ImplItemFn) -> Option<Box<Type>> {
find_parameters_type_in_sig(&fn_item.sig) 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. /// 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 /// ## Usage
/// ///
/// | field | type | usage | /// | field | type | usage |
/// | :- | :- | :- | /// | :- | :- | :- |
/// | `router` | `Ident` | The name of the router function to be generated. Defaults to `tool_router`. | /// | `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. | /// | `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 /// ## Example
/// ///
@ -62,18 +65,33 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> TokenStream {
/// impl MyToolHandler { /// impl MyToolHandler {
/// #[tool] /// #[tool]
/// pub fn my_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: /// Or specify the visibility and router name, which would be helpful when you want to combine multiple routers into one:
/// ///
/// ```rust,ignore /// ```rust,ignore
@ -114,50 +132,62 @@ pub fn tool_router(attr: TokenStream, input: TokenStream) -> TokenStream {
/// # tool_handler /// # 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 /// ## Usage
/// ///
/// | field | type | usage | /// | field | type | usage |
/// | :- | :- | :- | /// | :- | :- | :- |
/// | `router` | `Expr` | The expression to access the `ToolRouter` instance. Defaults to `self.tool_router`. | /// | `router` | `Expr` | The expression to access the `ToolRouter` instance. Defaults to `Self::tool_router()`. |
/// ## Example /// | `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 /// ```rust,ignore
/// #[tool_handler] /// struct TimeServer;
/// impl ServerHandler for MyToolHandler { ///
/// // ...implement other handler /// #[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 /// ```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 { /// impl ServerHandler for MyToolHandler {
/// // ...implement other handler /// // ...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 /// ```rust,ignore
/// #[tool_handler]
/// impl ServerHandler for MyToolHandler { /// impl ServerHandler for MyToolHandler {
/// async fn call_tool( /// fn get_info(&self) -> ServerInfo {
/// &self, /// ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
/// 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))
/// } /// }
/// } /// }
/// ``` /// ```
@ -237,13 +267,16 @@ pub fn prompt_router(attr: TokenStream, input: TokenStream) -> TokenStream {
/// # prompt_handler /// # 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 /// ## Usage
/// ///
/// | field | type | 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 /// ## Example
/// ```rust,ignore /// ```rust,ignore
@ -255,7 +288,7 @@ pub fn prompt_router(attr: TokenStream, input: TokenStream) -> TokenStream {
/// ///
/// or using a custom router expression: /// or using a custom router expression:
/// ```rust,ignore /// ```rust,ignore
/// #[prompt_handler(router = self.get_prompt_router())] /// #[prompt_handler(router = self.prompt_router)]
/// impl ServerHandler for MyPromptHandler { /// impl ServerHandler for MyPromptHandler {
/// // ...implement other handler methods /// // ...implement other handler methods
/// } /// }

View file

@ -3,6 +3,11 @@ use proc_macro2::TokenStream;
use quote::quote; use quote::quote;
use syn::{Expr, ImplItem, ItemImpl, parse_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)] #[derive(FromMeta, Debug, Default)]
#[darling(default)] #[darling(default)]
pub struct PromptHandlerAttribute { pub struct PromptHandlerAttribute {
@ -22,7 +27,7 @@ pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> syn::Result<Toke
let router_expr = attribute let router_expr = attribute
.router .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 // Add get_prompt implementation
let get_prompt_impl: ImplItem = parse_quote! { 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); 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! { Ok(quote! {
#impl_block #impl_block
}) })

View file

@ -3,6 +3,8 @@ use proc_macro2::TokenStream;
use quote::{ToTokens, quote}; use quote::{ToTokens, quote};
use syn::{Expr, ImplItem, ItemImpl}; use syn::{Expr, ImplItem, ItemImpl};
use crate::common::{has_method, has_sibling_handler};
#[derive(FromMeta)] #[derive(FromMeta)]
#[darling(default)] #[darling(default)]
struct TaskHandlerAttribute { struct TaskHandlerAttribute {
@ -20,14 +22,7 @@ impl Default for TaskHandlerAttribute {
pub fn task_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> { pub fn task_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
let attr_args = NestedMeta::parse_meta_list(attr)?; let attr_args = NestedMeta::parse_meta_list(attr)?;
let TaskHandlerAttribute { processor } = TaskHandlerAttribute::from_list(&attr_args)?; let TaskHandlerAttribute { processor } = TaskHandlerAttribute::from_list(&attr_args)?;
let mut item_impl = syn::parse2::<ItemImpl>(input.clone())?; let mut item_impl = syn::parse2::<ItemImpl>(input)?;
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,
})
};
if !has_method("list_tasks", &item_impl) { if !has_method("list_tasks", &item_impl) {
let list_fn = quote! { 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)?); 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()) Ok(item_impl.into_token_stream())
} }

View file

@ -3,39 +3,57 @@ use proc_macro2::TokenStream;
use quote::{ToTokens, quote}; use quote::{ToTokens, quote};
use syn::{Expr, ImplItem, ItemImpl}; use syn::{Expr, ImplItem, ItemImpl};
use crate::common::{has_method, has_sibling_handler};
#[derive(FromMeta)] #[derive(FromMeta)]
#[darling(default)] #[darling(default)]
pub struct ToolHandlerAttribute { pub struct ToolHandlerAttribute {
pub router: Expr, pub router: Expr,
pub meta: Option<Expr>, pub meta: Option<Expr>,
pub name: Option<String>,
pub version: Option<String>,
pub instructions: Option<String>,
} }
impl Default for ToolHandlerAttribute { impl Default for ToolHandlerAttribute {
fn default() -> Self { fn default() -> Self {
Self { Self {
router: syn::parse2(quote! { router: syn::parse2(quote! {
self.tool_router Self::tool_router()
}) })
.unwrap(), .unwrap(),
meta: None, meta: None,
name: None,
version: None,
instructions: None,
} }
} }
} }
pub fn tool_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> { pub fn tool_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
let attr_args = NestedMeta::parse_meta_list(attr)?; let attr_args = NestedMeta::parse_meta_list(attr)?;
let ToolHandlerAttribute { router, meta } = ToolHandlerAttribute::from_list(&attr_args)?; let ToolHandlerAttribute {
let mut item_impl = syn::parse2::<ItemImpl>(input.clone())?; router,
let tool_call_fn = quote! { meta,
async fn call_tool( name,
&self, version,
request: rmcp::model::CallToolRequestParams, instructions,
context: rmcp::service::RequestContext<rmcp::RoleServer>, } = ToolHandlerAttribute::from_list(&attr_args)?;
) -> Result<rmcp::model::CallToolResult, rmcp::ErrorData> { let mut item_impl = syn::parse2::<ItemImpl>(input)?;
let tcc = rmcp::handler::server::tool::ToolCallContext::new(self, request, context);
#router.call(tcc).await 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 { let result_meta = if let Some(meta) = meta {
quote! { Some(#meta) } quote! { Some(#meta) }
@ -43,31 +61,109 @@ pub fn tool_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS
quote! { None } quote! { None }
}; };
let tool_list_fn = quote! { if !has_method("list_tools", &item_impl) {
async fn list_tools( let tool_list_fn = syn::parse2::<ImplItem>(quote! {
&self, async fn list_tools(
_request: Option<rmcp::model::PaginatedRequestParams>, &self,
_context: rmcp::service::RequestContext<rmcp::RoleServer>, _request: Option<rmcp::model::PaginatedRequestParams>,
) -> Result<rmcp::model::ListToolsResult, rmcp::ErrorData> { _context: rmcp::service::RequestContext<rmcp::RoleServer>,
Ok(rmcp::model::ListToolsResult{ ) -> Result<rmcp::model::ListToolsResult, rmcp::ErrorData> {
tools: #router.list_all(), Ok(rmcp::model::ListToolsResult{
meta: #result_meta, tools: #router.list_all(),
next_cursor: None, meta: #result_meta,
}) next_cursor: None,
} })
}; }
})?;
item_impl.items.push(tool_list_fn);
}
let get_tool_fn = quote! { if !has_method("get_tool", &item_impl) {
fn get_tool(&self, name: &str) -> Option<rmcp::model::Tool> { let get_tool_fn = syn::parse2::<ImplItem>(quote! {
#router.get(name).cloned() 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()) 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 //! Procedural macro implementation for `#[tool_router]` (see `lib.rs`).
//! #[rmcp::tool_router(router)]
//! impl Handler {
//!
//! }
//! ```
//! //!
//! 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 darling::{FromMeta, ast::NestedMeta};
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
@ -16,6 +14,9 @@ use syn::{Ident, ImplItem, ItemImpl, Visibility};
pub struct ToolRouterAttribute { pub struct ToolRouterAttribute {
pub router: Ident, pub router: Ident,
pub vis: Option<Visibility>, 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 { impl Default for ToolRouterAttribute {
@ -23,14 +24,19 @@ impl Default for ToolRouterAttribute {
Self { Self {
router: format_ident!("tool_router"), router: format_ident!("tool_router"),
vis: None, vis: None,
server_handler: false,
} }
} }
} }
pub fn tool_router(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> { pub fn tool_router(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
let attr_args = NestedMeta::parse_meta_list(attr)?; let attr_args = NestedMeta::parse_meta_list(attr)?;
let ToolRouterAttribute { router, vis } = ToolRouterAttribute::from_list(&attr_args)?; let ToolRouterAttribute {
let mut item_impl = syn::parse2::<ItemImpl>(input.clone())?; router,
vis,
server_handler,
} = ToolRouterAttribute::from_list(&attr_args)?;
let mut item_impl = syn::parse2::<ItemImpl>(input)?;
// find all function marked with `#[rmcp::tool]` // find all function marked with `#[rmcp::tool]`
let tool_attr_fns: Vec<_> = item_impl let tool_attr_fns: Vec<_> = item_impl
.items .items
@ -52,7 +58,7 @@ pub fn tool_router(attr: TokenStream, input: TokenStream) -> syn::Result<TokenSt
} }
}) })
.collect(); .collect();
let mut routers = vec![]; let mut routers = Vec::with_capacity(tool_attr_fns.len());
for handler in tool_attr_fns { for handler in tool_attr_fns {
let tool_attr_fn_ident = format_ident!("{handler}_tool_attr"); let tool_attr_fn_ident = format_ident!("{handler}_tool_attr");
routers.push(quote! { routers.push(quote! {
@ -66,26 +72,69 @@ pub fn tool_router(attr: TokenStream, input: TokenStream) -> syn::Result<TokenSt
} }
})?; })?;
item_impl.items.push(router_fn); 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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
#[test] #[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! { let attr = quote! {
router = test_router, router = test_router,
vis = "pub(crate)" vis = "pub(crate)"
}; };
let attr_args = NestedMeta::parse_meta_list(attr)?; let attr_args = NestedMeta::parse_meta_list(attr)?;
let ToolRouterAttribute { router, vis } = ToolRouterAttribute::from_list(&attr_args)?; let ToolRouterAttribute {
println!("router: {}", router); router,
if let Some(vis) = vis { vis,
println!("visibility: {}", vis.to_token_stream()); server_handler,
} else { } = ToolRouterAttribute::from_list(&attr_args)?;
println!("visibility: None"); 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(()) Ok(())
} }
} }

View file

@ -10,9 +10,8 @@
//! # schemars //! # schemars
//! # }; //! # };
//! # use serde::{Serialize, Deserialize}; //! # use serde::{Serialize, Deserialize};
//! struct Server { //! struct Server;
//! tool_router: ToolRouter<Self>, //!
//! }
//! #[derive(Deserialize, schemars::JsonSchema, Default)] //! #[derive(Deserialize, schemars::JsonSchema, Default)]
//! struct AddParameter { //! struct AddParameter {
//! left: usize, //! left: usize,
@ -22,7 +21,7 @@
//! struct AddOutput { //! struct AddOutput {
//! sum: usize //! sum: usize
//! } //! }
//! #[tool_router] //! #[tool_router(server_handler)]
//! impl Server { //! impl Server {
//! #[tool(name = "adder", description = "Modular add two integers")] //! #[tool(name = "adder", description = "Modular add two integers")]
//! fn add( //! fn add(
@ -34,6 +33,11 @@
//! } //! }
//! ``` //! ```
//! //!
//! The `server_handler` flag emits `#[tool_handler]` for you (tools-only servers). For custom
//! `#[tool_handler(...)]` options or multiple handler macros on one `impl ServerHandler`, write
//! `#[tool_router]` and `#[tool_handler] impl ServerHandler for ...` explicitly—see
//! [`tool_router`][crate::tool_router] and [`tool_handler`][crate::tool_handler].
//!
//! Using the macro-based code pattern above is suitable for small MCP servers with simple interfaces. //! Using the macro-based code pattern above is suitable for small MCP servers with simple interfaces.
//! When the business logic become larger, it is recommended that each tool should reside //! When the business logic become larger, it is recommended that each tool should reside
//! in individual file, combined into MCP server using [`SyncTool`] and [`AsyncTool`] traits. //! in individual file, combined into MCP server using [`SyncTool`] and [`AsyncTool`] traits.

View file

@ -30,7 +30,7 @@ impl TestPromptServer {
} }
} }
#[prompt_handler] #[prompt_handler(router = self.prompt_router)]
impl ServerHandler for TestPromptServer {} impl ServerHandler for TestPromptServer {}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -80,7 +80,7 @@ impl<T: Send + Sync + 'static> GenericPromptServer<T> {
} }
} }
#[prompt_handler] #[prompt_handler(router = self.prompt_router)]
impl<T: Send + Sync + 'static> ServerHandler for GenericPromptServer<T> {} impl<T: Send + Sync + 'static> ServerHandler for GenericPromptServer<T> {}
#[test] #[test]
@ -148,7 +148,7 @@ mod nested {
} }
} }
#[prompt_handler] #[prompt_handler(router = self.prompt_router)]
impl ServerHandler for NestedServer {} impl ServerHandler for NestedServer {}
#[test] #[test]

View file

@ -19,7 +19,7 @@ mod tests {
format!("Direct: {}", input) format!("Direct: {}", input)
} }
} }
#[tool_handler] #[tool_handler(router = self.tool_router)]
impl ServerHandler for AnnotatedServer {} impl ServerHandler for AnnotatedServer {}
#[test] #[test]

View file

@ -10,7 +10,7 @@ use std::sync::Arc;
use rmcp::{ use rmcp::{
ClientHandler, ServerHandler, ServiceExt, ClientHandler, ServerHandler, ServiceExt,
handler::server::{router::tool::ToolRouter, wrapper::Parameters}, handler::server::{router::tool::ToolRouter, wrapper::Parameters},
model::{CallToolRequestParams, ClientInfo}, model::{CallToolRequestParams, ClientInfo, ServerCapabilities, ServerInfo},
tool, tool_handler, tool_router, tool, tool_handler, tool_router,
}; };
use schemars::JsonSchema; use schemars::JsonSchema;
@ -365,3 +365,211 @@ async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> {
server_handle.await??; server_handle.await??;
Ok(()) Ok(())
} }
// --- Tests for field-free minimal server pattern (issue #711) ---
/// Minimal server: no tool_router field, no new(), no get_info().
#[derive(Debug, Clone)]
pub struct MinimalServer;
#[tool_router]
impl MinimalServer {
#[tool(description = "Say hello")]
fn hello(&self) -> String {
"hello".to_string()
}
}
#[tool_handler]
impl ServerHandler for MinimalServer {}
#[test]
fn test_minimal_server_get_info_auto_generated() {
let server = MinimalServer;
let info = server.get_info();
assert!(
info.capabilities.tools.is_some(),
"tools capability should be enabled"
);
assert!(
info.capabilities.prompts.is_none(),
"prompts should not be auto-enabled"
);
assert!(
info.capabilities.tasks.is_none(),
"tasks should not be auto-enabled"
);
assert!(
!info.server_info.name.is_empty(),
"server name should not be empty"
);
assert!(
!info.server_info.version.is_empty(),
"server version should not be empty"
);
assert!(
info.instructions.is_none(),
"instructions should be None by default"
);
}
#[tokio::test]
async fn test_minimal_server_tool_call() -> anyhow::Result<()> {
let (server_transport, client_transport) = tokio::io::duplex(4096);
let server_handle = tokio::spawn(async move {
MinimalServer
.serve(server_transport)
.await?
.waiting()
.await?;
anyhow::Ok(())
});
let client = DummyClientHandler::default()
.serve(client_transport)
.await?;
let result = client
.call_tool(CallToolRequestParams::new("hello"))
.await?;
let text = result
.content
.first()
.and_then(|c| c.raw.as_text())
.map(|t| t.text.as_str())
.expect("Expected text content");
assert_eq!(text, "hello");
client.cancel().await?;
server_handle.await??;
Ok(())
}
/// Same minimal pattern as [`MinimalServer`], but `#[tool_handler]` is omitted using
/// `#[tool_router(server_handler)]` (emits `#[tool_handler]` for a second macro pass).
#[derive(Debug, Clone)]
pub struct ElidedToolHandlerServer;
#[tool_router(server_handler)]
impl ElidedToolHandlerServer {
#[tool(description = "Say hi")]
fn hi(&self) -> String {
"hi".to_string()
}
}
#[test]
fn test_tool_router_server_handler_flag_matches_minimal_server_get_info() {
let server = ElidedToolHandlerServer;
let info = server.get_info();
assert!(info.capabilities.tools.is_some());
assert!(
info.capabilities.prompts.is_none(),
"prompts should not be auto-enabled"
);
}
#[tokio::test]
async fn test_tool_router_server_handler_flag_end_to_end_tool_call() -> anyhow::Result<()> {
let (server_transport, client_transport) = tokio::io::duplex(4096);
let server_handle = tokio::spawn(async move {
ElidedToolHandlerServer
.serve(server_transport)
.await?
.waiting()
.await?;
anyhow::Ok(())
});
let client = DummyClientHandler::default()
.serve(client_transport)
.await?;
let result = client.call_tool(CallToolRequestParams::new("hi")).await?;
let text = result
.content
.first()
.and_then(|c| c.raw.as_text())
.map(|t| t.text.as_str())
.expect("Expected text content");
assert_eq!(text, "hi");
client.cancel().await?;
server_handle.await??;
Ok(())
}
/// Server with custom name/version/instructions via tool_handler attributes.
#[derive(Debug, Clone)]
pub struct CustomInfoServer;
#[tool_router]
impl CustomInfoServer {
#[tool(description = "Ping")]
fn ping(&self) -> String {
"pong".to_string()
}
}
#[tool_handler(
name = "my-custom-server",
version = "2.0.0",
instructions = "A custom server"
)]
impl ServerHandler for CustomInfoServer {}
#[test]
fn test_custom_info_server() {
let server = CustomInfoServer;
let info = server.get_info();
assert_eq!(info.server_info.name, "my-custom-server");
assert_eq!(info.server_info.version, "2.0.0");
assert_eq!(info.instructions.as_deref(), Some("A custom server"));
assert!(info.capabilities.tools.is_some());
}
/// Server that provides its own get_info() — macro should not override it.
#[derive(Debug, Clone)]
pub struct ManualInfoServer;
#[tool_router]
impl ManualInfoServer {
#[tool(description = "Noop")]
fn noop(&self) {}
}
#[tool_handler]
impl ServerHandler for ManualInfoServer {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(
ServerCapabilities::builder()
.enable_tools()
.enable_resources()
.build(),
)
.with_server_info(rmcp::model::Implementation::new("manual", "9.9.9"))
}
}
#[test]
fn test_manual_get_info_not_overridden() {
let server = ManualInfoServer;
let info = server.get_info();
assert_eq!(info.server_info.name, "manual");
assert_eq!(info.server_info.version, "9.9.9");
assert!(info.capabilities.tools.is_some());
assert!(
info.capabilities.resources.is_some(),
"manual resources should be preserved"
);
}

View file

@ -22,6 +22,7 @@
## 目录 ## 目录
- [使用](#使用) - [使用](#使用)
- [工具](#工具)
- [资源](#资源) - [资源](#资源)
- [提示词](#提示词) - [提示词](#提示词)
- [采样](#采样) - [采样](#采样)
@ -129,6 +130,76 @@ let quit_reason = server.cancel().await?;
--- ---
## 工具
工具允许服务端向客户端暴露可调用的函数。每个工具都有名称、描述和参数的 JSON Schema。客户端通过 `list_tools` 发现工具,通过 `call_tool` 调用工具。
**MCP 规范:** [Tools](https://modelcontextprotocol.io/specification/2025-11-25/server/tools)
### 服务端
`#[tool]`、`#[tool_router]` 和 `#[tool_handler]` 宏负责所有连接工作。对于纯工具服务端,可以使用 `#[tool_router(server_handler)]` 来省略单独的 `ServerHandler` 实现:
```rust,ignore
use rmcp::{tool, tool_router, ServiceExt, transport::stdio};
#[derive(Clone)]
struct Calculator;
#[tool_router(server_handler)]
impl Calculator {
#[tool(description = "Add two numbers")]
fn add(&self, #[tool(param)] a: i32, #[tool(param)] b: i32) -> String {
(a + b).to_string()
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let service = Calculator.serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
```
当需要自定义服务端元数据或多种能力(工具 + 提示词)时,使用显式的 `#[tool_handler]`
```rust,ignore
use rmcp::{tool, tool_router, tool_handler, ServerHandler, ServiceExt};
#[derive(Clone)]
struct Calculator;
#[tool_router]
impl Calculator {
#[tool(description = "Add two numbers")]
fn add(&self, #[tool(param)] a: i32, #[tool(param)] b: i32) -> String {
(a + b).to_string()
}
}
#[tool_handler(name = "calculator", version = "1.0.0", instructions = "A simple calculator")]
impl ServerHandler for Calculator {}
```
完整的宏文档请参阅 [`crates/rmcp-macros`](../../crates/rmcp-macros/README.md)。
### 客户端
```rust,ignore
use rmcp::model::CallToolRequestParams;
// 列出所有工具
let tools = client.list_all_tools().await?;
// 按名称调用工具
let result = client.call_tool(CallToolRequestParams::new("add")).await?;
```
**示例:** [`examples/servers/src/common/calculator.rs`](../../examples/servers/src/common/calculator.rs)(服务端),[`examples/servers/src/calculator_stdio.rs`](../../examples/servers/src/calculator_stdio.rs)stdio 运行器)
---
## 资源 ## 资源
资源允许服务端向客户端暴露数据文件、数据库记录、API 响应)供其读取。每个资源通过 URI 标识返回文本或二进制base64 编码)内容。资源模板允许服务端声明带有动态参数的 URI 模式。 资源允许服务端向客户端暴露数据文件、数据库记录、API 响应)供其读取。每个资源通过 URI 标识返回文本或二进制base64 编码)内容。资源模板允许服务端声明带有动态参数的 URI 模式。

View file

@ -17,7 +17,7 @@ async fn main() -> Result<()> {
tracing::info!("Starting Calculator MCP server"); tracing::info!("Starting Calculator MCP server");
// Create an instance of our calculator router // Create an instance of our calculator router
let service = Calculator::new().serve(stdio()).await.inspect_err(|e| { let service = Calculator.serve(stdio()).await.inspect_err(|e| {
tracing::error!("serving error: {:?}", e); tracing::error!("serving error: {:?}", e);
})?; })?;

View file

@ -1,11 +1,6 @@
#![allow(dead_code)] #![allow(dead_code)]
use rmcp::{ use rmcp::{handler::server::wrapper::Parameters, schemars, tool, tool_router};
ServerHandler,
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
model::{ServerCapabilities, ServerInfo},
schemars, tool, tool_handler, tool_router,
};
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] #[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SumRequest { pub struct SumRequest {
@ -23,18 +18,10 @@ pub struct SubRequest {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Calculator { pub struct Calculator;
tool_router: ToolRouter<Self>,
}
#[tool_router] #[tool_router(server_handler)]
impl Calculator { impl Calculator {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
#[tool(description = "Calculate the sum of two numbers")] #[tool(description = "Calculate the sum of two numbers")]
fn sum(&self, Parameters(SumRequest { a, b }): Parameters<SumRequest>) -> String { fn sum(&self, Parameters(SumRequest { a, b }): Parameters<SumRequest>) -> String {
(a + b).to_string() (a + b).to_string()
@ -45,11 +32,3 @@ impl Calculator {
(a - b).to_string() (a - b).to_string()
} }
} }
#[tool_handler]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
.with_instructions("A simple calculator".to_string())
}
}

View file

@ -1,10 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use rmcp::{ use rmcp::{
ServerHandler, ServerHandler, handler::server::wrapper::Parameters, schemars, tool, tool_handler, tool_router,
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
model::{ServerCapabilities, ServerInfo},
schemars, tool, tool_handler, tool_router,
}; };
#[allow(dead_code)] #[allow(dead_code)]
@ -41,7 +38,6 @@ impl DataService for MemoryDataService {
pub struct GenericService<DS: DataService> { pub struct GenericService<DS: DataService> {
#[allow(dead_code)] #[allow(dead_code)]
data_service: Arc<DS>, data_service: Arc<DS>,
tool_router: ToolRouter<Self>,
} }
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)] #[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
@ -55,7 +51,6 @@ impl<DS: DataService> GenericService<DS> {
pub fn new(data_service: DS) -> Self { pub fn new(data_service: DS) -> Self {
Self { Self {
data_service: Arc::new(data_service), data_service: Arc::new(data_service),
tool_router: Self::tool_router(),
} }
} }
@ -74,10 +69,5 @@ impl<DS: DataService> GenericService<DS> {
} }
} }
#[tool_handler] #[tool_handler(instructions = "generic data service")]
impl<DS: DataService> ServerHandler for GenericService<DS> { impl<DS: DataService> ServerHandler for GenericService<DS> {}
fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
.with_instructions("generic data service".to_string())
}
}

View file

@ -6,8 +6,8 @@ use std::{
use futures::Stream; use futures::Stream;
use rmcp::{ use rmcp::{
ErrorData as McpError, RoleServer, ServerHandler, handler::server::tool::ToolRouter, model::*, ErrorData as McpError, RoleServer, ServerHandler, model::*, service::RequestContext, tool,
service::RequestContext, tool, tool_handler, tool_router, tool_handler, tool_router,
}; };
use serde_json::json; use serde_json::json;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
@ -54,7 +54,6 @@ impl Stream for StreamDataSource {
#[derive(Clone)] #[derive(Clone)]
pub struct ProgressDemo { pub struct ProgressDemo {
data_source: StreamDataSource, data_source: StreamDataSource,
tool_router: ToolRouter<Self>,
} }
#[tool_router] #[tool_router]
@ -62,7 +61,6 @@ impl ProgressDemo {
#[allow(dead_code)] #[allow(dead_code)]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
tool_router: Self::tool_router(),
data_source: StreamDataSource::from_text("Hello, world!"), data_source: StreamDataSource::from_text("Hello, world!"),
} }
} }

View file

@ -2,11 +2,7 @@
use rmcp::{ use rmcp::{
ServerHandler, ServerHandler,
handler::server::{ handler::server::wrapper::{Json, Parameters},
router::tool::ToolRouter,
wrapper::{Json, Parameters},
},
model::{ServerCapabilities, ServerInfo},
schemars, tool, tool_handler, tool_router, schemars, tool, tool_handler, tool_router,
}; };
@ -26,17 +22,7 @@ pub struct SubRequest {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Calculator { pub struct Calculator;
tool_router: ToolRouter<Self>,
}
impl Calculator {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
}
#[tool_router] #[tool_router]
impl Calculator { impl Calculator {
@ -50,10 +36,5 @@ impl Calculator {
Json(a - b) Json(a - b)
} }
} }
#[tool_handler] #[tool_handler(instructions = "A simple calculator")]
impl ServerHandler for Calculator { impl ServerHandler for Calculator {}
fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
.with_instructions("A simple calculator")
}
}

View file

@ -24,7 +24,7 @@ async fn main() -> anyhow::Result<()> {
async fn http_server(req: Request<Incoming>) -> Result<hyper::Response<String>, hyper::Error> { async fn http_server(req: Request<Incoming>) -> Result<hyper::Response<String>, hyper::Error> {
tokio::spawn(async move { tokio::spawn(async move {
let upgraded = hyper::upgrade::on(req).await?; let upgraded = hyper::upgrade::on(req).await?;
let service = Calculator::new().serve(TokioIo::new(upgraded)).await?; let service = Calculator.serve(TokioIo::new(upgraded)).await?;
service.waiting().await?; service.waiting().await?;
anyhow::Result::<()>::Ok(()) anyhow::Result::<()>::Ok(())
}); });

View file

@ -16,7 +16,7 @@ async fn main() -> anyhow::Result<()> {
let stream = server; let stream = server;
server = ServerOptions::new().create(name)?; server = ServerOptions::new().create(name)?;
tokio::spawn(async move { tokio::spawn(async move {
match serve_server(Calculator::new(), stream).await { match serve_server(Calculator, stream).await {
Ok(server) => { Ok(server) => {
println!("Server initialized successfully"); println!("Server initialized successfully");
if let Err(e) = server.waiting().await { if let Err(e) = server.waiting().await {

View file

@ -13,7 +13,7 @@ async fn server() -> anyhow::Result<()> {
let tcp_listener = tokio::net::TcpListener::bind("127.0.0.1:8001").await?; let tcp_listener = tokio::net::TcpListener::bind("127.0.0.1:8001").await?;
while let Ok((stream, _)) = tcp_listener.accept().await { while let Ok((stream, _)) = tcp_listener.accept().await {
tokio::spawn(async move { tokio::spawn(async move {
let server = serve_server(Calculator::new(), stream).await?; let server = serve_server(Calculator, stream).await?;
server.waiting().await?; server.waiting().await?;
anyhow::Ok(()) anyhow::Ok(())
}); });

View file

@ -14,7 +14,7 @@ async fn main() -> anyhow::Result<()> {
while let Ok((stream, addr)) = unix_listener.accept().await { while let Ok((stream, addr)) = unix_listener.accept().await {
println!("Client connected: {:?}", addr); println!("Client connected: {:?}", addr);
tokio::spawn(async move { tokio::spawn(async move {
match serve_server(Calculator::new(), stream).await { match serve_server(Calculator, stream).await {
Ok(server) => { Ok(server) => {
println!("Server initialized successfully"); println!("Server initialized successfully");
if let Err(e) = server.waiting().await { if let Err(e) = server.waiting().await {

View file

@ -40,7 +40,7 @@ async fn start_server() -> anyhow::Result<()> {
tokio::spawn(async move { tokio::spawn(async move {
let ws_stream = tokio_tungstenite::accept_async(stream).await?; let ws_stream = tokio_tungstenite::accept_async(stream).await?;
let transport = WebsocketTransport::new_server(ws_stream); let transport = WebsocketTransport::new_server(ws_stream);
let server = Calculator::new().serve(transport).await?; let server = Calculator.serve(transport).await?;
server.waiting().await?; server.waiting().await?;
Ok::<(), anyhow::Error>(()) Ok::<(), anyhow::Error>(())
}); });

View file

@ -1,13 +1,8 @@
#![allow(dead_code)] #![allow(dead_code)]
use rmcp::{ use rmcp::{
ServerHandler, handler::server::wrapper::{Json, Parameters},
handler::server::{ schemars, tool, tool_router,
router::tool::ToolRouter,
wrapper::{Json, Parameters},
},
model::{ServerCapabilities, ServerInfo},
schemars, tool, tool_handler, tool_router,
}; };
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] #[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
@ -25,26 +20,10 @@ pub struct SubRequest {
pub b: i32, pub b: i32,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub struct Calculator { pub struct Calculator;
tool_router: ToolRouter<Self>,
}
impl Calculator { #[tool_router(server_handler)]
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
}
impl Default for Calculator {
fn default() -> Self {
Self::new()
}
}
#[tool_router]
impl Calculator { impl Calculator {
#[tool(description = "Calculate the sum of two numbers")] #[tool(description = "Calculate the sum of two numbers")]
fn sum(&self, Parameters(SumRequest { a, b }): Parameters<SumRequest>) -> String { fn sum(&self, Parameters(SumRequest { a, b }): Parameters<SumRequest>) -> String {
@ -56,11 +35,3 @@ impl Calculator {
Json(a - b) Json(a - b)
} }
} }
#[tool_handler]
impl ServerHandler for Calculator {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
.with_instructions("A simple calculator")
}
}

View file

@ -112,10 +112,7 @@ impl wasi::exports::cli::run::Guest for TokioCliRunner {
.with_writer(std::io::stderr) .with_writer(std::io::stderr)
.with_ansi(false) .with_ansi(false)
.init(); .init();
let server = calculator::Calculator::new() let server = calculator::Calculator.serve(wasi_io()).await.unwrap();
.serve(wasi_io())
.await
.unwrap();
server.waiting().await.unwrap(); server.waiting().await.unwrap();
}); });
Ok(()) Ok(())