Skip to content

Commit 61ffba8

Browse files
authored
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.
1 parent 53cd5ed commit 61ffba8

4 files changed

Lines changed: 59 additions & 2 deletions

File tree

crates/rmcp/src/handler/server/router/prompt.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,9 @@ where
187187
}
188188

189189
pub fn list_all(&self) -> Vec<crate::model::Prompt> {
190-
self.map.values().map(|item| item.attr.clone()).collect()
190+
let mut prompts: Vec<_> = self.map.values().map(|item| item.attr.clone()).collect();
191+
prompts.sort_by(|a, b| a.name.cmp(&b.name));
192+
prompts
191193
}
192194
}
193195

crates/rmcp/src/handler/server/router/tool.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,9 @@ where
252252
}
253253

254254
pub fn list_all(&self) -> Vec<crate::model::Tool> {
255-
self.map.values().map(|item| item.attr.clone()).collect()
255+
let mut tools: Vec<_> = self.map.values().map(|item| item.attr.clone()).collect();
256+
tools.sort_by(|a, b| a.name.cmp(&b.name));
257+
tools
256258
}
257259

258260
/// Get a tool definition by name.

crates/rmcp/tests/test_prompt_routers.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,39 @@ fn test_prompt_router() {
103103
let prompts = test_prompt_router.list_all();
104104
assert_eq!(prompts.len(), 4);
105105
}
106+
107+
#[test]
108+
fn test_prompt_router_list_all_is_sorted() {
109+
let router = TestHandler::<()>::test_router()
110+
.with_route(rmcp::handler::server::router::prompt::PromptRoute::new_dyn(
111+
async_function_prompt_attr(),
112+
|mut context| {
113+
Box::pin(async move {
114+
use rmcp::handler::server::{
115+
common::FromContextPart, prompt::IntoGetPromptResult,
116+
};
117+
let params = Parameters::<Request>::from_context_part(&mut context)?;
118+
let result = async_function(params).await;
119+
result.into_get_prompt_result()
120+
})
121+
},
122+
))
123+
.with_route(rmcp::handler::server::router::prompt::PromptRoute::new_dyn(
124+
async_function2_prompt_attr(),
125+
|context| {
126+
Box::pin(async move {
127+
use rmcp::handler::server::prompt::IntoGetPromptResult;
128+
let result = async_function2(context.server).await;
129+
result.into_get_prompt_result()
130+
})
131+
},
132+
));
133+
let prompts = router.list_all();
134+
let names: Vec<&str> = prompts.iter().map(|p| p.name.as_ref()).collect();
135+
let mut sorted = names.clone();
136+
sorted.sort();
137+
assert_eq!(
138+
names, sorted,
139+
"list_all() should return prompts sorted alphabetically by name"
140+
);
141+
}

crates/rmcp/tests/test_tool_routers.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,20 @@ where
6666
H: CallToolHandler<S, A>,
6767
{
6868
}
69+
70+
#[test]
71+
fn test_tool_router_list_all_is_sorted() {
72+
let router: ToolRouter<TestHandler<()>> = ToolRouter::<TestHandler<()>>::new()
73+
.with_route((async_function_tool_attr(), async_function))
74+
.with_route((async_function2_tool_attr(), async_function2))
75+
+ TestHandler::<()>::test_router_1()
76+
+ TestHandler::<()>::test_router_2();
77+
let tools = router.list_all();
78+
let names: Vec<&str> = tools.iter().map(|t| t.name.as_ref()).collect();
79+
let mut sorted = names.clone();
80+
sorted.sort();
81+
assert_eq!(
82+
names, sorted,
83+
"list_all() should return tools sorted alphabetically by name"
84+
);
85+
}

0 commit comments

Comments
 (0)