Repository navigation
StreamableHttpClientTransport hangs on call_tool when server uses json_response + stateless mode #754
Description
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
mcp_connect(initialize + initialized) succeeds — no issues with session setupwrite_file,read_file,list_dirall work viacall_toolexechangs indefinitely viacall_tool, even as the very first call after initialization- The server does return a correct
200 OKwithapplication/jsoncontent-type and valid JSON-RPC response body — confirmed via curl and server-side tracing - The same server works correctly with the official Python MCP SDK (
mcppackage,streamablehttp_client) — all tools includingexecrespond instantly - The hang also occurs with
stateful_mode: trueon the server (withoutallow_statelesson the client) - The hang also occurs regardless of whether
reqwest0.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
Activity
NicksLameCode commentedon Mar 19, 2026
Thanks for the detailed repro — I opened a fix in #762.
Root cause: in direct-JSON response mode, the server could return the first emitted message from the oneshot transport rather than the terminal JSON-RPC response. If a tool emitted progress/notifications before completing, the HTTP body could contain that notification, leaving the client still waiting on the original request ID.
Test evidence:
stateless_json_response_waits_for_terminal_tool_responsejson_responsebehavior with a progress-emitting toolcall_toolcompletes successfullycargo test --all-featurespassesThe fix updates the JSON-direct path to drain intermediate notifications and wait for the terminal
Response/Errorbefore building the HTTP response.github-actions commentedon Jul 22, 2026
This issue has had no activity for 60 days and is now marked as stale. It will be closed in 14 days if there is no further activity. Add the
keeplabel to keep it open.alexhancock commentedon Jul 28, 2026
This is very likely resolved in the soon-to-be-released 3.0. We have a beta out now - check releases. Feel free to re-open if it is still an issue.