Skip to content

StreamableHttpClientTransport hangs on call_tool when server uses json_response + stateless mode #754

@EItanya

Description

@EItanya

Description

When using StreamableHttpClientTransport to connect to a StreamableHTTP MCP server that runs in stateless mode (stateful_mode: false) with json_response: true, the call_tool method hangs indefinitely for specific tools, even though the server returns a correct 200 JSON response.

Reproduction

Server setup

An MCP server using rmcp 1.2.0 with StreamableHttpServerConfig:

let config = StreamableHttpServerConfig {
    stateful_mode: false,
    json_response: true,
    ..Default::default()
};

The server implements several tools including exec (registered as a dynamic route via ToolRoute::new_dyn) and write_file, read_file, list_dir (registered as standard tool methods).

Client code

use rmcp::transport::streamable_http_client::{
    StreamableHttpClientTransport, StreamableHttpClientTransportConfig,
};

let config = StreamableHttpClientTransportConfig {
    uri: "http://localhost:8080/mcp".into(),
    allow_stateless: true,
    ..Default::default()
};
let transport = StreamableHttpClientTransport::from_config(config);
let session = rmcp::serve_client((), transport).await?;

// This works:
session.call_tool(CallToolRequestParams::new("write_file").with_arguments(args)).await?;

// This hangs forever:
session.call_tool(CallToolRequestParams::new("exec").with_arguments(args)).await?;

Observations

  1. mcp_connect (initialize + initialized) succeeds — no issues with session setup
  2. write_file, read_file, list_dir all work via call_tool
  3. exec hangs indefinitely via call_tool, even as the very first call after initialization
  4. The server does return a correct 200 OK with application/json content-type and valid JSON-RPC response body — confirmed via curl and server-side tracing
  5. The same server works correctly with the official Python MCP SDK (mcp package, streamablehttp_client) — all tools including exec respond instantly
  6. The hang also occurs with stateful_mode: true on the server (without allow_stateless on the client)
  7. The hang also occurs regardless of whether reqwest 0.12 or 0.13 is used

Server response comparison

Both write_file and exec return identical response formats:

# write_file response
HTTP/1.1 200 OK
content-type: application/json
content-length: 112
{"jsonrpc":"2.0","id":10,"result":{"content":[{"type":"text","text":"wrote 1 bytes to a.txt"}],"isError":false}}

# exec response  
HTTP/1.1 200 OK
content-type: application/json
content-length: 144
{"jsonrpc":"2.0","id":11,"result":{"content":[{"type":"text","text":"{\"stdout\":\"hello\",\"stderr\":\"\",\"exit_code\":0}"}],"isError":false}}

The only difference is the text content (exec returns JSON-encoded output as a string). Both are valid JSON-RPC responses with correct content-length.

Key difference

The exec tool is registered as a dynamic route via ToolRoute::new_dyn (because it needs access to ToolCallContext for progress notifications), while other tools are standard #[tool] methods. However, this shouldn't affect the HTTP response format, and indeed the responses are identical as shown above.

Environment

  • rmcp version: 1.2.0
  • Rust: stable
  • OS: Linux (Debian)
  • reqwest: 0.13.2 (also reproduced with 0.12.28)

Comment left by Claude on behalf of @EItanya

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions