Bug Description
In Streamable HTTP stateless mode (with_stateful_mode(false)), tools/list requests never reach the ServerHandler::list_tools() implementation. Instead, only tools from the first #[tool_router] block are returned. The stdio transport correctly calls ServerHandler::list_tools() and returns all tools.
Reproduction
Server with two #[tool_router] blocks merged via instance field:
#[derive(Debug, Clone)]
struct MyServer {
tool_router: ToolRouter<Self>,
}
impl MyServer {
fn new() -> Self {
let mut router = Self::core_tools(); // 27 tools
router.merge(Self::extra_tools()); // 15 tools
// router now has 42 tools
Self { tool_router: router }
}
}
#[tool_router(router = core_tools, vis = "pub")]
impl MyServer {
// ... 27 #[tool] methods ...
}
#[tool_router(router = extra_tools, vis = "pub")]
impl MyServer {
// ... 15 #[tool] methods ...
}
#[tool_handler(router = self.tool_router)]
impl ServerHandler for MyServer {}
Expected
tools/list via HTTP returns 42 tools (same as stdio).
Actual
| Transport |
tools/list count |
ServerHandler::list_tools called? |
--stdio (via ServiceExt::serve) |
42 ✅ |
Yes |
| Streamable HTTP stateless |
27 ❌ |
No (verified with eprintln — never fires) |
Investigation
merged_router() returns 42 at runtime (startup log confirms)
Clone preserves 42 tools (manual Clone impl with logging confirms)
- Manually implementing
list_tools on ServerHandler (no #[tool_handler] macro) — still 27 via HTTP
- Adding
eprintln! inside list_tools — never printed for HTTP requests
- Same
eprintln! does print for stdio requests
call_tool also fails for tools in the second block ("tool not found")
- Renaming the field (to avoid
tool_router name collision with generated fn) doesn't help
#[tool_handler(router = Self::merged_router())] vs #[tool_handler(router = self.tool_router)] — neither works for HTTP
Root Cause Hypothesis
The serve_directly code path used by StreamableHttpService in stateless mode appears to dispatch tools/list and tools/call through a mechanism that bypasses ServerHandler::list_tools() entirely. The impl<H: ServerHandler> Service<RoleServer> for H blanket impl calls self.list_tools(), and this IS the path used by stdio — but somehow NOT by the HTTP stateless transport.
Environment
- rmcp: 1.6.0 (also confirmed on HEAD of main — same code in
crates/rmcp-macros/src/tool_handler.rs)
- Rust: stable (1.87)
- OS: macOS ARM64
- Config:
StreamableHttpServerConfig::default().with_stateful_mode(false).with_json_response(true)
Workaround
Use stdio transport (--stdio flag) which correctly dispatches to ServerHandler::list_tools(). Configure clients to connect via command: (stdio) instead of http_url:.
Impact
Any MCP server using:
- Multiple
#[tool_router] blocks (merged via field)
- OR custom
ServerHandler::list_tools() override
- AND Streamable HTTP transport in stateless mode
...will silently return incomplete tool lists and fail tools/call for tools not in the first block. This is particularly impactful for larger servers with 27+ tools.
Bug Description
In Streamable HTTP stateless mode (
with_stateful_mode(false)),tools/listrequests never reach theServerHandler::list_tools()implementation. Instead, only tools from the first#[tool_router]block are returned. The stdio transport correctly callsServerHandler::list_tools()and returns all tools.Reproduction
Server with two
#[tool_router]blocks merged via instance field:Expected
tools/listvia HTTP returns 42 tools (same as stdio).Actual
--stdio(viaServiceExt::serve)Investigation
merged_router()returns 42 at runtime (startup log confirms)Clonepreserves 42 tools (manual Clone impl with logging confirms)list_toolsonServerHandler(no#[tool_handler]macro) — still 27 via HTTPeprintln!insidelist_tools— never printed for HTTP requestseprintln!does print for stdio requestscall_toolalso fails for tools in the second block ("tool not found")tool_routername collision with generated fn) doesn't help#[tool_handler(router = Self::merged_router())]vs#[tool_handler(router = self.tool_router)]— neither works for HTTPRoot Cause Hypothesis
The
serve_directlycode path used byStreamableHttpServicein stateless mode appears to dispatchtools/listandtools/callthrough a mechanism that bypassesServerHandler::list_tools()entirely. Theimpl<H: ServerHandler> Service<RoleServer> for Hblanket impl callsself.list_tools(), and this IS the path used by stdio — but somehow NOT by the HTTP stateless transport.Environment
crates/rmcp-macros/src/tool_handler.rs)StreamableHttpServerConfig::default().with_stateful_mode(false).with_json_response(true)Workaround
Use stdio transport (
--stdioflag) which correctly dispatches toServerHandler::list_tools(). Configure clients to connect viacommand:(stdio) instead ofhttp_url:.Impact
Any MCP server using:
#[tool_router]blocks (merged via field)ServerHandler::list_tools()override...will silently return incomplete tool lists and fail
tools/callfor tools not in the first block. This is particularly impactful for larger servers with 27+ tools.