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 setup
write_file, read_file, list_dir all work via call_tool
exec hangs indefinitely via call_tool, even as the very first call after initialization
- 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
- The same server works correctly with the official Python MCP SDK (
mcp package, streamablehttp_client) — all tools including exec respond instantly
- The hang also occurs with
stateful_mode: true on the server (without allow_stateless on the client)
- 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
Description
When using
StreamableHttpClientTransportto connect to a StreamableHTTP MCP server that runs in stateless mode (stateful_mode: false) withjson_response: true, thecall_toolmethod hangs indefinitely for specific tools, even though the server returns a correct 200 JSON response.Reproduction
Server setup
An MCP server using
rmcp1.2.0 withStreamableHttpServerConfig:The server implements several tools including
exec(registered as a dynamic route viaToolRoute::new_dyn) andwrite_file,read_file,list_dir(registered as standard tool methods).Client code
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 initialization200 OKwithapplication/jsoncontent-type and valid JSON-RPC response body — confirmed via curl and server-side tracingmcppackage,streamablehttp_client) — all tools includingexecrespond instantlystateful_mode: trueon the server (withoutallow_statelesson the client)reqwest0.12 or 0.13 is usedServer response comparison
Both
write_fileandexecreturn identical response formats: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
exectool is registered as a dynamic route viaToolRoute::new_dyn(because it needs access toToolCallContextfor 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
Comment left by Claude on behalf of @EItanya