One MCP tool call can consume the entire turn budget, because only connect and list are bounded #141

Closed
opened 2026-08-12 10:25:26 +00:00 by coilyco-ops · 0 comments
Member

The gap

internal/community/mcp.go bounds two of the three MCP phases:

mcpConnectTimeout = 10 * time.Second
mcpListTimeout    = 15 * time.Second

mcpToolSession.Call has no bound of its own. It passes the turn context straight to CallTool, so the only ceiling is RequestTimeout, which defaults to three minutes. The shared http.Client in NewAgent also carries Timeout: cfg.RequestTimeout, so the transport ceiling is the same three minutes.

Why that shape is the problem

The turn budget and the tool transport expire at the same moment. A tool that hangs takes the whole turn with it and leaves nothing to report the failure with.

That is the 180 second forgejo MCP hang recorded in #138, trace 883ba8968cd370c32d678168dbad68b5, which produced an orphaned root span and no Discord message. 180 seconds is exactly defaultRequestTimeout, which is the tell: the tool did not fail, the turn did.

Connect and list already have this right. A tool call is the one phase where a remote server can block indefinitely, and it is the one phase left unbounded.

Scope

Give the tool call its own ceiling, well below the turn budget, so a hung server fails as a tool failure with turn budget still on the clock. Follow the existing RefreshInterval shape so the bound is configurable per provider and defaulted in one place.

Six tool rounds still fit inside the turn budget, so the turn timeout remains the outer bound. This only stops one call from being able to spend all of it.

Complete when

  • A tool call that never returns fails on its own timeout rather than on the turn's.
  • The failure reaches the member as a tool failure, which #138 already routes.
  • Connect and list bounds are unchanged.
## The gap `internal/community/mcp.go` bounds two of the three MCP phases: ```go mcpConnectTimeout = 10 * time.Second mcpListTimeout = 15 * time.Second ``` `mcpToolSession.Call` has no bound of its own. It passes the turn context straight to `CallTool`, so the only ceiling is `RequestTimeout`, which defaults to three minutes. The shared `http.Client` in `NewAgent` also carries `Timeout: cfg.RequestTimeout`, so the transport ceiling is the same three minutes. ## Why that shape is the problem The turn budget and the tool transport expire at the same moment. A tool that hangs takes the whole turn with it and leaves nothing to report the failure with. That is the 180 second forgejo MCP hang recorded in #138, trace `883ba8968cd370c32d678168dbad68b5`, which produced an orphaned root span and no Discord message. 180 seconds is exactly `defaultRequestTimeout`, which is the tell: the tool did not fail, the turn did. Connect and list already have this right. A tool call is the one phase where a remote server can block indefinitely, and it is the one phase left unbounded. ## Scope Give the tool call its own ceiling, well below the turn budget, so a hung server fails as a tool failure with turn budget still on the clock. Follow the existing `RefreshInterval` shape so the bound is configurable per provider and defaulted in one place. Six tool rounds still fit inside the turn budget, so the turn timeout remains the outer bound. This only stops one call from being able to spend all of it. ## Complete when * A tool call that never returns fails on its own timeout rather than on the turn's. * The failure reaches the member as a tool failure, which #138 already routes. * Connect and list bounds are unchanged.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo#141
No description provided.