Skip to content

Streamable HTTP stateless mode ignores ServerHandler::list_tools() — tools/list returns only first #[tool_router] block #844

@MikkoParkkola

Description

@MikkoParkkola

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_toolsnever 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions