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:
parent
53cd5ed84a
commit
61ffba84b5
4 changed files with 59 additions and 2 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue