fix: sort list_all() output in ToolRouter and PromptRouter for deterministic ordering (#665)

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.
This commit is contained in:
Peter 2026-02-17 15:37:17 +01:00 • committed by GitHub
commit 61ffba84b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 59 additions and 2 deletions

View file

@ -187,7 +187,9 @@ where
}
pub fn list_all(&self) -> Vec<crate::model::Prompt> {
self.map.values().map(|item| item.attr.clone()).collect()
let mut prompts: Vec<_> = self.map.values().map(|item| item.attr.clone()).collect();
prompts.sort_by(|a, b| a.name.cmp(&b.name));
prompts
}
}

View file

@ -252,7 +252,9 @@ where
}
pub fn list_all(&self) -> Vec<crate::model::Tool> {
self.map.values().map(|item| item.attr.clone()).collect()
let mut tools: Vec<_> = self.map.values().map(|item| item.attr.clone()).collect();
tools.sort_by(|a, b| a.name.cmp(&b.name));
tools
}
/// Get a tool definition by name.

View file

@ -103,3 +103,39 @@ fn test_prompt_router() {
let prompts = test_prompt_router.list_all();
assert_eq!(prompts.len(), 4);
}
#[test]
fn test_prompt_router_list_all_is_sorted() {
let router = TestHandler::<()>::test_router()
.with_route(rmcp::handler::server::router::prompt::PromptRoute::new_dyn(
async_function_prompt_attr(),
|mut context| {
Box::pin(async move {
use rmcp::handler::server::{
common::FromContextPart, prompt::IntoGetPromptResult,
};
let params = Parameters::<Request>::from_context_part(&mut context)?;
let result = async_function(params).await;
result.into_get_prompt_result()
})
},
))
.with_route(rmcp::handler::server::router::prompt::PromptRoute::new_dyn(
async_function2_prompt_attr(),
|context| {
Box::pin(async move {
use rmcp::handler::server::prompt::IntoGetPromptResult;
let result = async_function2(context.server).await;
result.into_get_prompt_result()
})
},
));
let prompts = router.list_all();
let names: Vec<&str> = prompts.iter().map(|p| p.name.as_ref()).collect();
let mut sorted = names.clone();
sorted.sort();
assert_eq!(
names, sorted,
"list_all() should return prompts sorted alphabetically by name"
);
}

View file

@ -66,3 +66,20 @@ 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"
);
}