ToolRouter::list_all() and PromptRouter::list_all() iterate over a HashMap, which returns items in non-deterministic order. Since list_all() backs the tools/list and prompts/list MCP protocol responses, this causes MCP clients to receive differently-ordered results across calls and process restarts, leading to intermittent tool discovery failures. Sort the output alphabetically by name to guarantee stable ordering.
85 lines
2.4 KiB
Rust
85 lines
2.4 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use futures::future::BoxFuture;
|
|
use rmcp::{
|
|
ServerHandler,
|
|
handler::server::{router::tool::ToolRouter, tool::CallToolHandler, wrapper::Parameters},
|
|
};
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct TestHandler<T: 'static = ()> {
|
|
pub _marker: std::marker::PhantomData<fn(*const T)>,
|
|
}
|
|
|
|
impl<T: 'static> ServerHandler for TestHandler<T> {}
|
|
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
|
|
pub struct Request {
|
|
pub fields: HashMap<String, String>,
|
|
}
|
|
|
|
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
|
|
pub struct Sum {
|
|
pub a: i32,
|
|
pub b: i32,
|
|
}
|
|
|
|
#[rmcp::tool_router(router = test_router_1)]
|
|
impl<T> TestHandler<T> {
|
|
#[rmcp::tool]
|
|
async fn async_method(&self, Parameters(Request { fields }): Parameters<Request>) {
|
|
drop(fields)
|
|
}
|
|
}
|
|
|
|
#[rmcp::tool_router(router = test_router_2)]
|
|
impl<T> TestHandler<T> {
|
|
#[rmcp::tool]
|
|
fn sync_method(&self, Parameters(Request { fields }): Parameters<Request>) {
|
|
drop(fields)
|
|
}
|
|
}
|
|
|
|
#[rmcp::tool]
|
|
async fn async_function(Parameters(Request { fields }): Parameters<Request>) {
|
|
drop(fields)
|
|
}
|
|
|
|
#[rmcp::tool]
|
|
fn async_function2<T>(_callee: &TestHandler<T>) -> BoxFuture<'_, ()> {
|
|
Box::pin(async move {})
|
|
}
|
|
|
|
#[test]
|
|
fn test_tool_router() {
|
|
let test_tool_router: ToolRouter<TestHandler<()>> = ToolRouter::<TestHandler<()>>::new()
|
|
.with_route((async_function_tool_attr(), async_function))
|
|
.with_route((async_function2_tool_attr(), async_function2))
|
|
+ TestHandler::<()>::test_router_1()
|
|
+ TestHandler::<()>::test_router_2();
|
|
let tools = test_tool_router.list_all();
|
|
assert_eq!(tools.len(), 4);
|
|
assert_handler(TestHandler::<()>::async_method);
|
|
}
|
|
|
|
fn assert_handler<S, H, A>(_handler: H)
|
|
where
|
|
H: CallToolHandler<S, A>,
|
|
{
|
|
}
|
|
|
|
#[test]
|
|
fn test_tool_router_list_all_is_sorted() {
|
|
let router: ToolRouter<TestHandler<()>> = ToolRouter::<TestHandler<()>>::new()
|
|
.with_route((async_function_tool_attr(), async_function))
|
|
.with_route((async_function2_tool_attr(), async_function2))
|
|
+ TestHandler::<()>::test_router_1()
|
|
+ TestHandler::<()>::test_router_2();
|
|
let tools = router.list_all();
|
|
let names: Vec<&str> = tools.iter().map(|t| t.name.as_ref()).collect();
|
|
let mut sorted = names.clone();
|
|
sorted.sort();
|
|
assert_eq!(
|
|
names, sorted,
|
|
"list_all() should return tools sorted alphabetically by name"
|
|
);
|
|
}
|