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
25 changed files with 743 additions and 234 deletions
71
README.md
71
README.md
|
|
@ -22,6 +22,7 @@ For the full MCP specification, see [modelcontextprotocol.io](https://modelconte
|
|||
## Table of Contents
|
||||
|
||||
- [Usage](#usage)
|
||||
- [Tools](#tools)
|
||||
- [Resources](#resources)
|
||||
- [Prompts](#prompts)
|
||||
- [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 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.
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,8 @@
|
|||
//! # schemars
|
||||
//! # };
|
||||
//! # use serde::{Serialize, Deserialize};
|
||||
//! struct Server {
|
||||
//! tool_router: ToolRouter<Self>,
|
||||
//! }
|
||||
//! struct Server;
|
||||
//!
|
||||
//! #[derive(Deserialize, schemars::JsonSchema, Default)]
|
||||
//! struct AddParameter {
|
||||
//! left: usize,
|
||||
|
|
@ -22,7 +21,7 @@
|
|||
//! struct AddOutput {
|
||||
//! sum: usize
|
||||
//! }
|
||||
//! #[tool_router]
|
||||
//! #[tool_router(server_handler)]
|
||||
//! impl Server {
|
||||
//! #[tool(name = "adder", description = "Modular add two integers")]
|
||||
//! 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.
|
||||
//! 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.
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ impl TestPromptServer {
|
|||
}
|
||||
}
|
||||
|
||||
#[prompt_handler]
|
||||
#[prompt_handler(router = self.prompt_router)]
|
||||
impl ServerHandler for TestPromptServer {}
|
||||
|
||||
#[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> {}
|
||||
|
||||
#[test]
|
||||
|
|
@ -148,7 +148,7 @@ mod nested {
|
|||
}
|
||||
}
|
||||
|
||||
#[prompt_handler]
|
||||
#[prompt_handler(router = self.prompt_router)]
|
||||
impl ServerHandler for NestedServer {}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ mod tests {
|
|||
format!("Direct: {}", input)
|
||||
}
|
||||
}
|
||||
#[tool_handler]
|
||||
#[tool_handler(router = self.tool_router)]
|
||||
impl ServerHandler for AnnotatedServer {}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use std::sync::Arc;
|
|||
use rmcp::{
|
||||
ClientHandler, ServerHandler, ServiceExt,
|
||||
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
|
||||
model::{CallToolRequestParams, ClientInfo},
|
||||
model::{CallToolRequestParams, ClientInfo, ServerCapabilities, ServerInfo},
|
||||
tool, tool_handler, tool_router,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
|
|
@ -365,3 +365,211 @@ async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> {
|
|||
server_handle.await??;
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 模式。
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ async fn main() -> Result<()> {
|
|||
tracing::info!("Starting Calculator MCP server");
|
||||
|
||||
// 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);
|
||||
})?;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use rmcp::{
|
||||
ServerHandler,
|
||||
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
|
||||
model::{ServerCapabilities, ServerInfo},
|
||||
schemars, tool, tool_handler, tool_router,
|
||||
};
|
||||
use rmcp::{handler::server::wrapper::Parameters, schemars, tool, tool_router};
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct SumRequest {
|
||||
|
|
@ -23,18 +18,10 @@ pub struct SubRequest {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Calculator {
|
||||
tool_router: ToolRouter<Self>,
|
||||
}
|
||||
pub struct Calculator;
|
||||
|
||||
#[tool_router]
|
||||
#[tool_router(server_handler)]
|
||||
impl Calculator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tool_router: Self::tool_router(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "Calculate the sum of two numbers")]
|
||||
fn sum(&self, Parameters(SumRequest { a, b }): Parameters<SumRequest>) -> String {
|
||||
(a + b).to_string()
|
||||
|
|
@ -45,11 +32,3 @@ impl Calculator {
|
|||
(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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use rmcp::{
|
||||
ServerHandler,
|
||||
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
|
||||
model::{ServerCapabilities, ServerInfo},
|
||||
schemars, tool, tool_handler, tool_router,
|
||||
ServerHandler, handler::server::wrapper::Parameters, schemars, tool, tool_handler, tool_router,
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
|
@ -41,7 +38,6 @@ impl DataService for MemoryDataService {
|
|||
pub struct GenericService<DS: DataService> {
|
||||
#[allow(dead_code)]
|
||||
data_service: Arc<DS>,
|
||||
tool_router: ToolRouter<Self>,
|
||||
}
|
||||
|
||||
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
|
||||
|
|
@ -55,7 +51,6 @@ impl<DS: DataService> GenericService<DS> {
|
|||
pub fn new(data_service: DS) -> Self {
|
||||
Self {
|
||||
data_service: Arc::new(data_service),
|
||||
tool_router: Self::tool_router(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,10 +69,5 @@ impl<DS: DataService> GenericService<DS> {
|
|||
}
|
||||
}
|
||||
|
||||
#[tool_handler]
|
||||
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())
|
||||
}
|
||||
}
|
||||
#[tool_handler(instructions = "generic data service")]
|
||||
impl<DS: DataService> ServerHandler for GenericService<DS> {}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ use std::{
|
|||
|
||||
use futures::Stream;
|
||||
use rmcp::{
|
||||
ErrorData as McpError, RoleServer, ServerHandler, handler::server::tool::ToolRouter, model::*,
|
||||
service::RequestContext, tool, tool_handler, tool_router,
|
||||
ErrorData as McpError, RoleServer, ServerHandler, model::*, service::RequestContext, tool,
|
||||
tool_handler, tool_router,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tokio_stream::StreamExt;
|
||||
|
|
@ -54,7 +54,6 @@ impl Stream for StreamDataSource {
|
|||
#[derive(Clone)]
|
||||
pub struct ProgressDemo {
|
||||
data_source: StreamDataSource,
|
||||
tool_router: ToolRouter<Self>,
|
||||
}
|
||||
|
||||
#[tool_router]
|
||||
|
|
@ -62,7 +61,6 @@ impl ProgressDemo {
|
|||
#[allow(dead_code)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tool_router: Self::tool_router(),
|
||||
data_source: StreamDataSource::from_text("Hello, world!"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,7 @@
|
|||
|
||||
use rmcp::{
|
||||
ServerHandler,
|
||||
handler::server::{
|
||||
router::tool::ToolRouter,
|
||||
wrapper::{Json, Parameters},
|
||||
},
|
||||
model::{ServerCapabilities, ServerInfo},
|
||||
handler::server::wrapper::{Json, Parameters},
|
||||
schemars, tool, tool_handler, tool_router,
|
||||
};
|
||||
|
||||
|
|
@ -26,17 +22,7 @@ pub struct SubRequest {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Calculator {
|
||||
tool_router: ToolRouter<Self>,
|
||||
}
|
||||
|
||||
impl Calculator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tool_router: Self::tool_router(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct Calculator;
|
||||
|
||||
#[tool_router]
|
||||
impl Calculator {
|
||||
|
|
@ -50,10 +36,5 @@ impl Calculator {
|
|||
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")
|
||||
}
|
||||
}
|
||||
#[tool_handler(instructions = "A simple calculator")]
|
||||
impl ServerHandler for Calculator {}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
async fn http_server(req: Request<Incoming>) -> Result<hyper::Response<String>, hyper::Error> {
|
||||
tokio::spawn(async move {
|
||||
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?;
|
||||
anyhow::Result::<()>::Ok(())
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
let stream = server;
|
||||
server = ServerOptions::new().create(name)?;
|
||||
tokio::spawn(async move {
|
||||
match serve_server(Calculator::new(), stream).await {
|
||||
match serve_server(Calculator, stream).await {
|
||||
Ok(server) => {
|
||||
println!("Server initialized successfully");
|
||||
if let Err(e) = server.waiting().await {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ async fn server() -> anyhow::Result<()> {
|
|||
let tcp_listener = tokio::net::TcpListener::bind("127.0.0.1:8001").await?;
|
||||
while let Ok((stream, _)) = tcp_listener.accept().await {
|
||||
tokio::spawn(async move {
|
||||
let server = serve_server(Calculator::new(), stream).await?;
|
||||
let server = serve_server(Calculator, stream).await?;
|
||||
server.waiting().await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
while let Ok((stream, addr)) = unix_listener.accept().await {
|
||||
println!("Client connected: {:?}", addr);
|
||||
tokio::spawn(async move {
|
||||
match serve_server(Calculator::new(), stream).await {
|
||||
match serve_server(Calculator, stream).await {
|
||||
Ok(server) => {
|
||||
println!("Server initialized successfully");
|
||||
if let Err(e) = server.waiting().await {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ async fn start_server() -> anyhow::Result<()> {
|
|||
tokio::spawn(async move {
|
||||
let ws_stream = tokio_tungstenite::accept_async(stream).await?;
|
||||
let transport = WebsocketTransport::new_server(ws_stream);
|
||||
let server = Calculator::new().serve(transport).await?;
|
||||
let server = Calculator.serve(transport).await?;
|
||||
server.waiting().await?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use rmcp::{
|
||||
ServerHandler,
|
||||
handler::server::{
|
||||
router::tool::ToolRouter,
|
||||
wrapper::{Json, Parameters},
|
||||
},
|
||||
model::{ServerCapabilities, ServerInfo},
|
||||
schemars, tool, tool_handler, tool_router,
|
||||
handler::server::wrapper::{Json, Parameters},
|
||||
schemars, tool, tool_router,
|
||||
};
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
|
|
@ -25,26 +20,10 @@ pub struct SubRequest {
|
|||
pub b: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Calculator {
|
||||
tool_router: ToolRouter<Self>,
|
||||
}
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Calculator;
|
||||
|
||||
impl Calculator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tool_router: Self::tool_router(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Calculator {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[tool_router]
|
||||
#[tool_router(server_handler)]
|
||||
impl Calculator {
|
||||
#[tool(description = "Calculate the sum of two numbers")]
|
||||
fn sum(&self, Parameters(SumRequest { a, b }): Parameters<SumRequest>) -> String {
|
||||
|
|
@ -56,11 +35,3 @@ impl Calculator {
|
|||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,10 +112,7 @@ impl wasi::exports::cli::run::Guest for TokioCliRunner {
|
|||
.with_writer(std::io::stderr)
|
||||
.with_ansi(false)
|
||||
.init();
|
||||
let server = calculator::Calculator::new()
|
||||
.serve(wasi_io())
|
||||
.await
|
||||
.unwrap();
|
||||
let server = calculator::Calculator.serve(wasi_io()).await.unwrap();
|
||||
server.waiting().await.unwrap();
|
||||
});
|
||||
Ok(())
|
||||
|
|
|
|||
Loading…
Reference in a new issue