Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions crates/rmcp-macros/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,10 @@ For **getting started** and **full MCP feature documentation**, see the [main RE
## Quick Example

```rust,ignore
use rmcp::{tool, tool_router, tool_handler, ServerHandler, model::*};
use rmcp::{tool, tool_router, tool_handler, ServerHandler};

#[derive(Clone)]
struct MyServer {
tool_router: rmcp::handler::server::tool::ToolRouter<Self>,
}
struct MyServer;

#[tool_router]
impl MyServer {
Expand All @@ -54,11 +52,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.
Expand Down
23 changes: 22 additions & 1 deletion crates/rmcp-macros/src/common.rs
Original file line number Diff line number Diff line change
@@ -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> {
Expand Down Expand Up @@ -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)
})
}
92 changes: 53 additions & 39 deletions crates/rmcp-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ 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 |
Expand All @@ -62,16 +64,13 @@ 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 {}
/// ```
///
/// Or specify the visibility and router name, which would be helpful when you want to combine multiple routers into one:
Expand Down Expand Up @@ -114,50 +113,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(name = "my-server", version = "1.0.0", instructions = "A helpful server")]
/// impl ServerHandler for MyToolHandler {}
/// ```
///
/// ## Custom router expression
///
/// ```rust,ignore
/// #[tool_handler(router = self.get_router().await)]
/// #[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())
/// }
/// }
/// ```
Expand Down Expand Up @@ -237,13 +248,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
Expand All @@ -255,7 +269,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
/// }
Expand Down
18 changes: 17 additions & 1 deletion crates/rmcp-macros/src/prompt_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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! {
Expand Down Expand Up @@ -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
})
Expand Down
27 changes: 19 additions & 8 deletions crates/rmcp-macros/src/task_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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! {
Expand Down Expand Up @@ -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())
}
Loading
Loading