Skip to content

refactor: refactor tool macros and router implementation #261

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

4t145
Copy link
Collaborator

@4t145 4t145 commented Jun 16, 2025

Motivation and Context

Refactor tool macros:

  1. Make it more intuitive and simple, no more attributes on function's parameter.
  2. Better support for generic handler.
  3. No static router.
  4. Composable macros.

How Has This Been Tested?

Breaking Changes

Now we need to use a combination of three macros:

tool

This macro is used to mark a function as a tool handler.

This will generate a function that return the attribute of this tool, with type rmcp::model::Tool.

Usage

feied type usage
name String The name of the tool. If not provided, it defaults to the function name.
description String A description of the tool. The document of this function will be used.
input_schema Expr A JSON Schema object defining the expected parameters for the tool. If not provide, if will use the json schema of its argument with type Parameters<T>
annotations ToolAnnotationsAttribute Additional tool information. Defaults to None.

Example

#[tool(name = "my_tool", description = "This is my tool", annotations(title = "我的工具", read_only_hint = true))]
pub async fn my_tool(param: Parameters<MyToolParam>) {
    // handling tool request
}

tool_router

This macro is used to generate a tool router based on functions marked with #[rmcp::tool] in an implementation block.

It creates a function that returns a ToolRouter instance.

Usage

feied 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.

Example

#[tool_router]
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(),
        }
    }
}

Or specify the visibility and router name:

#[tool_router(router = my_tool_router, vis = pub)]
impl MyToolHandler {
    #[tool]
    pub fn my_tool() {
        
    }
}

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.

Usage

field type usage
router Expr The expression to access the ToolRouter instance. Defaults to self.tool_router.

Example

#[tool_handler]
impl ServerHandler for MyToolHandler {
    // ...implement other handler
}

or using a custom router expression:

#[tool_handler(router = self.get_router().await)]
impl ServerHandler for MyToolHandler {
   // ...implement other handler
}

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

This also can be a template for possible prompt handler in the future. And also can be a base of implementation of #[derive(ServerHandler)]

4t145 added 7 commits June 16, 2025 19:21
- Updated the `#[tool(tool_box)]` macro to `#[tool_router]` across various modules for consistency.
- Enhanced the `Calculator`, `Counter`, and `GenericService` structs to utilize `ToolRouter` for handling tool calls.
- Introduced `Parameters` struct for better parameter handling in tool functions.
- Added new methods for listing tools and calling tools in server handlers.
- Improved test cases to reflect changes in tool routing and parameter handling.
- Updated documentation and examples to align with the new router structure.
@4t145 4t145 marked this pull request as ready for review June 17, 2025 07:49
@4t145 4t145 requested review from Copilot and jokemanfire and removed request for Copilot June 17, 2025 07:50
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the tool macros and router implementation to improve intuitiveness, composability, and generic handler support while removing the static router. Key changes include updating the macros (tool, tool_router, and tool_handler) and modifying test code to use the new API (e.g. using Calculator::new() and Parameters for function arguments).

Reviewed Changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
crates/rmcp/tests/test_with_js.rs Changed instantiation of Calculator using Calculator::new().
crates/rmcp/tests/test_tool_routers.rs Added new router functions (test_router_1, test_router_2) and updated tool macros usage.
crates/rmcp/tests/test_tool_macros.rs Updated tool macros usage in tests, including changes in function parameter patterns and router setup.
crates/rmcp/src/* Refactored tool_router integration and related macro-generated code; added helper functions (e.g. with_all_items) in model.
crates/rmcp-macros/* Updated macro implementations for tool, tool_router, and tool_handler to support the new API style.
Comments suppressed due to low confidence (1)

crates/rmcp/tests/test_tool_macros.rs:110

  • [nitpick] The naming of generated tool attribute functions (e.g. get_weather_tool_attr) and their usage in tests should be reviewed for consistency. Ensure that the naming conventions across the tool macros and their invocations remain uniform to avoid confusion.
server.get_weather(Parameters(GetWeatherRequest { city: "Harbin".into(), date: "Yesterday".into() }))

@@ -382,174 +312,118 @@ macro_rules! impl_for {
impl_for!([$($Tn)* $Tn_1] [$($Rest)*]);
};
(@impl $($Tn: ident)*) => {
impl<'s, $($Tn,)* S, F, Fut, R> CallToolHandler<'s, S, AsyncAdapter<($($Tn,)*), Fut, R>> for F
impl<$($Tn,)* S, F, R> CallToolHandler<S, AsyncMethodAdapter<($($Tn,)*), R>> for F
Copy link
Preview

Copilot AI Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The adapter implementations for CallToolHandler (including AsyncMethodAdapter and SyncAdapter) are quite complex. Consider refactoring the inner logic or adding inline comments to clarify the lifetime conversions and function adapter patterns for future maintainers.

Copilot uses AI. Check for mistakes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take it easy

@4t145
Copy link
Collaborator Author

4t145 commented Jun 18, 2025

@jokemanfire It's ready for review now

@github-actions github-actions bot added T-documentation Documentation improvements T-dependencies Dependencies related changes T-test Testing related changes T-config Configuration file changes T-core Core library changes T-examples Example code changes T-handler Handler implementation changes T-macros Macro changes labels Jun 18, 2025
@4t145
Copy link
Collaborator Author

4t145 commented Jun 18, 2025

@jokemanfire

We can collect docs like this:

// extract doc line from attribute
fn extract_doc_line(existing_docs: Option<String>, attr: &syn::Attribute) -> Option<String> {
    if !attr.path().is_ident("doc") {
        return None;
    }

    let syn::Meta::NameValue(name_value) = &attr.meta else {
        return None;
    };

    let syn::Expr::Lit(expr_lit) = &name_value.value else {
        return None;
    };

    let syn::Lit::Str(lit_str) = &expr_lit.lit else {
        return None;
    };

    let content = lit_str.value().trim().to_string();
    match (existing_docs, content) {
        (Some(mut existing_docs), content) if !content.is_empty() => {
            existing_docs.push('\n');
            existing_docs.push_str(&content);
            Some(existing_docs)
        }
        (Some(existing_docs), _) => Some(existing_docs),
        (None, content) if !content.is_empty() => Some(content),
        _ => None,
    }
}

and

fn_item.attrs.iter().fold(None, extract_doc_line)

And check again please.

@jokemanfire
Copy link
Collaborator

I will give a feedback tonight.

@@ -26,7 +26,7 @@ fn calculator(&self, #[tool(param)] a: i32, #[tool(param)] b: i32) -> Result<Cal
Use on an impl block to automatically register multiple tools:

```rust ignore
#[tool(tool_box)]
#[tool_router]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like tool_router 's expand is in func 'new' , The struct must have 'ToolRouter', I think it should be write in this readme

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not always, a static router is also ok. I will add more examples.

_request: Option<rmcp::model::PaginatedRequestParam>,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<ListToolsResult, rmcp::Error> {
let items = self.tool_router.list_all();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The marco 'tool_handler' will expand the call_tool and list_tools func , why we need clarify it again?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will use #[tool_handler] as much as possible, but remain one place to explain what it looks like after expanded

@jokemanfire
Copy link
Collaborator

The tool_handler marco looks like not really using?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-config Configuration file changes T-core Core library changes T-dependencies Dependencies related changes T-documentation Documentation improvements T-examples Example code changes T-handler Handler implementation changes T-macros Macro changes T-test Testing related changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants