Skip to content

Bound MCP tool calls with their own watchdog timeout - #528

Merged
TheGreatAxios merged 2 commits into
mainfrom
cl-6895-hung-mcp-tool-calls-stall-turns-forever-mcp-calls-need-their
Aug 22, 2026
Merged

Bound MCP tool calls with their own watchdog timeout#528
TheGreatAxios merged 2 commits into
mainfrom
cl-6895-hung-mcp-tool-calls-stall-turns-forever-mcp-calls-need-their

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Collaborator

Summary

  • A wedged MCP server previously hung a tool call forever: the generic tool watchdog no longer arms by default, and the stall watchdog treats any in-flight tool as activity, so nothing bounded a wedged mcp__* call.
  • mcp__* calls now go through their own branch in resolveToolExecutionTimeoutMs (src/tui/tool-execution-watchdog.ts) that arms unconditionally — default 5 minutes (DEFAULT_MCP_TOOL_TIMEOUT_MS), configurable via settings.mcp.timeoutMs, still capped by tools.maxTimeoutMs when set. task and run_shell behavior is unchanged.
  • On expiry the call returns a normal, model-reactable tool-error result: MCP tool <name> timed out after <n>s — the server may be wedged; retry or continue without it (formatMcpToolTimeoutMessage in src/plugins/tool-time-budget.ts). The turn is never aborted.
  • Settings plumbing: Settings.mcp.timeoutMs (src/config/settings.ts), folded into toolWatchdogFromSettings's existing return shape as mcpTimeoutMs.

Concurrency investigation (CL-6895)

Read src/mcp/client.ts and the vendored @modelcontextprotocol/sdk's Protocol.request() (node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js). Requests are keyed by an incrementing JSON-RPC message id in a Map, with per-request abort/timeout handling — there is no serial queue or mutex around outgoing requests in either our code or the SDK, so N parallel calls to one server should not deadlock each other at the client layer.

Live forensics for this issue showed the hung calls eventually completing (multi-minute, but not indefinite), which is consistent with a slow MCP server response rather than a client-side deadlock. No fix applied here beyond the timeout bound; if a real deadlock resurfaces it likely lives in the vendored SDK or the server itself, not in src/mcp/client.ts.

Per-tool elapsed-time surfacing and notice-clearing for long-running tools are tracked separately (CL-6894) and are out of scope here.

Test plan

  • bun test src/tui/tool-execution-watchdog.test.ts — new cases: mcp default timeout with no config, mcp.timeoutMs override, tools.maxTimeoutMs still caps it, a never-resolving mock MCP tool times out with a model-reactable error while the runner keeps working, concurrent MCP calls time out independently of each other and of a fast sibling call.
  • bun test src/settings.test.tstoolWatchdogFromSettings maps mcp.timeoutMs alone and merged with tools.*.
  • bun test src/tui/dynamic-tool-runner.test.ts src/mcp/ src/tui/tool-rows.test.ts — unaffected, all pass.
  • bunx tsc --noEmit -p . clean.
  • Confirmed (via stream-event-map.ts / turns-to-blocks.ts) that a timed-out tool result (isError: true) flows through the existing error-row paint path — no separate TUI change needed.

Fixes CL-6895
https://linear.app/abklabs/issue/CL-6895

A wedged MCP server previously hung a tool call (and the stall watchdog
treats an in-flight tool as activity, so the turn) forever, since the
generic tool watchdog no longer arms by default. mcp__* calls now arm
unconditionally through resolveToolExecutionTimeoutMs, defaulting to 5
minutes (settings.mcp.timeoutMs to override, capped by tools.maxTimeoutMs),
and time out with a normal model-reactable tool error instead of hanging.

Investigated the MCP client for a concurrency deadlock: our client.ts and
the vendored SDK's Protocol.request() multiplex requests by JSON-RPC id
with no serial queue or mutex, so parallel calls to one server should not
deadlock each other. Live forensics for this issue showed multi-minute MCP
calls that later completed, consistent with a slow server rather than a
client-side deadlock.

Fixes CL-6895
https://linear.app/abklabs/issue/CL-6895
@linear-code

linear-code Bot commented Aug 22, 2026

Copy link
Copy Markdown

CL-6895

@TheGreatAxios
TheGreatAxios merged commit d9d516a into main Aug 22, 2026
3 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant