Skip to content

Commit 6c336a9

Browse files
authored
feat: add trait-based tool declaration (#677)
* feat: add trait-based tool declaration * fix: typo * fix: add docs, make more idomatic patterns, allow for empty parameters and return types * fix: format code * fix: add default trait * fix: docs typo
1 parent 332fcbf commit 6c336a9

File tree

5 files changed

+518
-5
lines changed

5 files changed

+518
-5
lines changed

crates/rmcp-macros/src/tool.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,7 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
238238
// if not found, use a default empty JSON schema object
239239
// TODO: should be updated according to the new specifications
240240
syn::parse2::<Expr>(quote! {
241-
std::sync::Arc::new(serde_json::json!({
242-
"type": "object",
243-
"properties": {}
244-
}).as_object().unwrap().clone())
241+
rmcp::handler::server::common::schema_for_empty_input()
245242
})?
246243
}
247244
};

crates/rmcp/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ async fn calculate(&self, params: Parameters<CalculationRequest>) -> Result<Json
130130
# }
131131
```
132132

133-
The `#[tool]` macro automatically generates an output schema from the `CalculationResult` type.
133+
The `#[tool]` macro automatically generates an output schema from the `CalculationResult` type. See the [documentation of `tool` module](crate::handler::server::router::tool) for more instructions.
134134

135135
## Tasks
136136

crates/rmcp/src/handler/server/common.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ pub fn schema_for_type<T: JsonSchema + std::any::Any>() -> Arc<JsonObject> {
4848
})
4949
}
5050

51+
// TODO: should be updated according to the new specifications
52+
/// Schema used when input is empty.
53+
pub fn schema_for_empty_input() -> Arc<JsonObject> {
54+
std::sync::Arc::new(
55+
serde_json::json!({
56+
"type": "object",
57+
"properties": {}
58+
})
59+
.as_object()
60+
.unwrap()
61+
.clone(),
62+
)
63+
}
64+
5165
/// Generate and validate a JSON schema for outputSchema (must have root type "object").
5266
pub fn schema_for_output<T: JsonSchema + std::any::Any>() -> Result<Arc<JsonObject>, String> {
5367
thread_local! {

crates/rmcp/src/handler/server/router/tool.rs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,132 @@
1+
//! Tools for MCP servers.
2+
//!
3+
//! It's straightforward to define tools using [`tool_router`][crate::tool_router] and
4+
//! [`tool`][crate::tool] macro.
5+
//!
6+
//! ```rust
7+
//! # use rmcp::{
8+
//! # tool_router, tool,
9+
//! # handler::server::{wrapper::{Parameters, Json}, tool::ToolRouter},
10+
//! # schemars
11+
//! # };
12+
//! # use serde::{Serialize, Deserialize};
13+
//! struct Server {
14+
//! tool_router: ToolRouter<Self>,
15+
//! }
16+
//! #[derive(Deserialize, schemars::JsonSchema, Default)]
17+
//! struct AddParameter {
18+
//! left: usize,
19+
//! right: usize
20+
//! }
21+
//! #[derive(Serialize, schemars::JsonSchema)]
22+
//! struct AddOutput {
23+
//! sum: usize
24+
//! }
25+
//! #[tool_router]
26+
//! impl Server {
27+
//! #[tool(name = "adder", description = "Modular add two integers")]
28+
//! fn add(
29+
//! &self,
30+
//! Parameters(AddParameter { left, right }): Parameters<AddParameter>
31+
//! ) -> Json<AddOutput> {
32+
//! Json(AddOutput { sum: left.wrapping_add(right) })
33+
//! }
34+
//! }
35+
//! ```
36+
//!
37+
//! Using the macro-based code pattern above is suitable for small MCP servers with simple interfaces.
38+
//! When the business logic become larger, it is recommended that each tool should reside
39+
//! in individual file, combined into MCP server using [`SyncTool`] and [`AsyncTool`] traits.
40+
//!
41+
//! ```rust
42+
//! # use rmcp::{
43+
//! # handler::server::{
44+
//! # tool::ToolRouter,
45+
//! # router::tool::{SyncTool, AsyncTool, ToolBase},
46+
//! # },
47+
//! # schemars, ErrorData
48+
//! # };
49+
//! # pub struct MyCustomError;
50+
//! # impl From<MyCustomError> for ErrorData {
51+
//! # fn from(err: MyCustomError) -> ErrorData { unimplemented!() }
52+
//! # }
53+
//! # use serde::{Serialize, Deserialize};
54+
//! # use std::borrow::Cow;
55+
//! // In tool1.rs
56+
//! pub struct ComplexTool1;
57+
//! #[derive(Deserialize, schemars::JsonSchema, Default)]
58+
//! pub struct ComplexTool1Input { /* ... */ }
59+
//! #[derive(Serialize, schemars::JsonSchema)]
60+
//! pub struct ComplexTool1Output { /* ... */ }
61+
//!
62+
//! impl ToolBase for ComplexTool1 {
63+
//! type Parameter = ComplexTool1Input;
64+
//! type Output = ComplexTool1Output;
65+
//! type Error = MyCustomError;
66+
//! fn name() -> Cow<'static, str> {
67+
//! "complex-tool1".into()
68+
//! }
69+
//!
70+
//! fn description() -> Option<Cow<'static, str>> {
71+
//! Some("...".into())
72+
//! }
73+
//! }
74+
//! impl SyncTool<MyToolServer> for ComplexTool1 {
75+
//! fn invoke(service: &MyToolServer, param: Self::Parameter) -> Result<Self::Output, Self::Error> {
76+
//! // ...
77+
//! # unimplemented!()
78+
//! }
79+
//! }
80+
//! // In tool2.rs
81+
//! pub struct ComplexTool2;
82+
//! #[derive(Deserialize, schemars::JsonSchema, Default)]
83+
//! pub struct ComplexTool2Input { /* ... */ }
84+
//! #[derive(Serialize, schemars::JsonSchema)]
85+
//! pub struct ComplexTool2Output { /* ... */ }
86+
//!
87+
//! impl ToolBase for ComplexTool2 {
88+
//! type Parameter = ComplexTool2Input;
89+
//! type Output = ComplexTool2Output;
90+
//! type Error = MyCustomError;
91+
//! fn name() -> Cow<'static, str> {
92+
//! "complex-tool2".into()
93+
//! }
94+
//!
95+
//! fn description() -> Option<Cow<'static, str>> {
96+
//! Some("...".into())
97+
//! }
98+
//! }
99+
//! impl AsyncTool<MyToolServer> for ComplexTool2 {
100+
//! async fn invoke(service: &MyToolServer, param: Self::Parameter) -> Result<Self::Output, Self::Error> {
101+
//! // ...
102+
//! # unimplemented!()
103+
//! }
104+
//! }
105+
//!
106+
//! // In tool_router.rs
107+
//! struct MyToolServer {
108+
//! tool_router: ToolRouter<Self>,
109+
//! }
110+
//! impl MyToolServer {
111+
//! pub fn tool_router() -> ToolRouter<Self> {
112+
//! ToolRouter::new()
113+
//! .with_sync_tool::<ComplexTool1>()
114+
//! .with_async_tool::<ComplexTool2>()
115+
//! }
116+
//! }
117+
//! ```
118+
//!
119+
//! It's also possible to use macro-based and trait-based tool definition together: Since
120+
//! [`ToolRouter`] implements [`Add`][std::ops::Add], you can add two tool routers into final
121+
//! router as showed in [the documentation of `tool_router`][crate::tool_router].
122+
123+
mod tool_traits;
124+
1125
use std::{borrow::Cow, sync::Arc};
2126

3127
use futures::{FutureExt, future::BoxFuture};
4128
use schemars::JsonSchema;
129+
pub use tool_traits::{AsyncTool, SyncTool, ToolBase};
5130

6131
use crate::{
7132
handler::server::{
@@ -219,6 +344,42 @@ where
219344
self
220345
}
221346

347+
/// Add a tool that implements [`SyncTool`]
348+
pub fn with_sync_tool<T>(self) -> Self
349+
where
350+
T: SyncTool<S> + 'static,
351+
{
352+
if T::input_schema().is_some() {
353+
self.with_route((
354+
tool_traits::tool_attribute::<T>(),
355+
tool_traits::sync_tool_wrapper::<S, T>,
356+
))
357+
} else {
358+
self.with_route((
359+
tool_traits::tool_attribute::<T>(),
360+
tool_traits::sync_tool_wrapper_with_empty_params::<S, T>,
361+
))
362+
}
363+
}
364+
365+
/// Add a tool that implements [`AsyncTool`]
366+
pub fn with_async_tool<T>(self) -> Self
367+
where
368+
T: AsyncTool<S> + 'static,
369+
{
370+
if T::input_schema().is_some() {
371+
self.with_route((
372+
tool_traits::tool_attribute::<T>(),
373+
tool_traits::async_tool_wrapper::<S, T>,
374+
))
375+
} else {
376+
self.with_route((
377+
tool_traits::tool_attribute::<T>(),
378+
tool_traits::async_tool_wrapper_with_empty_params::<S, T>,
379+
))
380+
}
381+
}
382+
222383
pub fn add_route(&mut self, item: ToolRoute<S>) {
223384
let new_name = &item.attr.name;
224385
validate_and_warn_tool_name(new_name);

0 commit comments

Comments
 (0)