zeroclaw-labs/zeroclaw · error · anyhow::Error
MCP server `{server_name}` timed out after {timeout_secs}s d
Error message
MCP server `{server_name}` timed out after {timeout_secs}s during {operation}; outcome unknown and request was not replayed What it means
The JSON-RPC request was written to the server but no response arrived within the configured timeout. Because the server may still be processing it, the client deliberately does not replay the request; it arms an asynchronous recovery (transport reset + re-handshake) and bails, telling the caller the operation may or may not have taken effect.
Source
Thrown at crates/zeroclaw-tools/src/mcp_client.rs:661
match send_result {
Err(_) => {
let unknown_epoch = lifecycle.outcome_unknown_epoch();
cancellation_guard.disarm();
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Timeout)
.with_outcome(::zeroclaw_log::EventOutcome::Failure)
.with_attrs(::serde_json::json!({
"mcp_server": &server_name,
"rpc_method": rpc_method,
"timeout_secs": timeout_secs,
"outcome_unknown": unknown_epoch.is_some(),
})),
"mcp_client: MCP request timed out"
);
if let Some(epoch) = unknown_epoch {
self.spawn_recovery(epoch, operation.to_string());
bail!(
"MCP server `{server_name}` timed out after {timeout_secs}s during \
{operation}; outcome unknown and request was not replayed"
);
}
bail!(
"MCP server `{server_name}` timed out after {timeout_secs}s before writing \
{operation}"
);
}
Ok(Ok(response)) => {
cancellation_guard.disarm();
return Ok(response);
}
Ok(Err(error)) => {
if let Some(epoch) = lifecycle.outcome_unknown_epoch() {
cancellation_guard.disarm();
self.spawn_recovery(epoch, operation.to_string());
return Err(error).with_context(|| {View on GitHub (pinned to 88bb9c8533)
Solutions
- Raise the per-tool timeout above the tool's realistic worst-case execution time
- Check server-side logs to learn whether the operation actually completed before deciding to retry
- Retry only idempotent operations directly; for writes, verify the effect on the server first — the client guarantees no automatic replay
- If it recurs, profile why the server is slow rather than only raising the ceiling
Example fix
# before tool_timeout_secs = 30 # after — sized to the slowest advertised tool tool_timeout_secs = 180
Defensive patterns
Strategy: retry
Type guard
fn is_outcome_unknown_timeout(err: &anyhow::Error) -> bool {
err.to_string().contains("outcome unknown and request was not replayed")
} Try / catch
On this error the operation is ambiguous: retry reads directly once recovery completes; for writes, query the server for the effect (list the changed state) and only then re-issue — never blind-retry, the client intentionally does not replay.
Prevention
- Set timeout_secs above each tool's realistic worst case (LLM-backed tools need minutes)
- Design tool calls to be idempotent or queryable so outcome-unknown is recoverable
- Watch the WARN 'MCP request timed out' events to tune timeouts before failures cascade
When it happens
Trigger: Slow tools (LLM-backed, large file scans) exceeding timeout_secs; a hung or deadlocked server; network drop after the request was sent; timeouts configured below the tool's documented worst case.
Common situations: Default timeouts used with heavyweight tools; servers that queue requests behind long jobs; transport stalls on loaded machines.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- MCP server `{server_name}` rejected initialize: {:?}
- MCP `{op}` (server `{server_name}`) returned isError: {detai
- MCP server `{server_name}` timed out after {timeout_secs}s b
- MCP server `{server_name}` exhausted the {timeout_secs}s bud
- MCP tool `{tool_name}` error {}: {}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/d361a6522e189abd.
Report an issue: GitHub.