-
Notifications
You must be signed in to change notification settings - Fork 478
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