Follow-up to the MCP death detection added in v1.3.2 (#353).
Problem
The v1.3.2 detection covers the stdio transport only. A Streamable HTTP MCP server that initializes successfully and later becomes unreachable stays marked ready: its tools keep being advertised to the model, FindToolServer keeps resolving them, and every call fails.
Two reasons:
- The reactive check in
internal/mcp/executor.go matches transport.ErrTransportClosed, which is defined in the stdio transport. The HTTP client in internal/mcp/client.go builds its errors with fresh fmt.Errorf wraps (mcp http do: %w around a *url.Error, or a formatted string for a non-2xx), so the match is never true.
- The proactive watcher is registered behind a
client.(interface{ Exited() <-chan struct{} }) assertion. Only the stdio client implements it, so no watcher is started for HTTP.
Observed against a closed httptest server after a successful handshake:
POST-DEATH errors.Is(err, transport.ErrTransportClosed) = false
POST-DEATH IsReady=true AllTools=[http_tool] FindToolServer=true
A server down at initialization is handled correctly — it never goes ready, and /readyz reports it. The gap is only post-initialization death.
Impact
Degraded rather than fatal: Executor returns the failure as a tool-result message, so the model sees the error and the request completes. The cost is a wasted round trip per call, and tools advertised that cannot be used.
With mcp_servers[].required: true the readiness gate is also affected — a required HTTP server that has died reports healthy, because readiness reads the same ready flag that never clears.
Why this was not fixed alongside the stdio case
Marking a server unready is terminal. There is no periodic re-probe; recovery requires a configuration reload or a restart. That is correct for a crashed subprocess, which cannot return on its own, but wrong for HTTP: a refused connection is routinely transient — a rolling deploy, a pod restart, a load balancer draining a backend.
Treating the first HTTP error as death would therefore turn a few seconds of redeployment into a permanent withdrawal of that server's tools, and with required: true into a permanent 503 that takes the instance out of rotation until an operator intervenes. That is worse than the bug.
Proposed
Both halves are needed; neither works alone.
- A consecutive-failure threshold (or a failure window) before an HTTP server is withdrawn, so a blip is distinguished from an outage.
- A periodic re-probe of unready servers, so withdrawal is recoverable without a configuration reload. This is worth having for stdio as well.
Related
AGENTS.md scopes the post-initialization death-detection guarantee to stdio transports. That note should be removed once this ships.
Follow-up to the MCP death detection added in v1.3.2 (#353).
Problem
The v1.3.2 detection covers the stdio transport only. A Streamable HTTP MCP server that initializes successfully and later becomes unreachable stays marked ready: its tools keep being advertised to the model,
FindToolServerkeeps resolving them, and every call fails.Two reasons:
internal/mcp/executor.gomatchestransport.ErrTransportClosed, which is defined in the stdio transport. The HTTP client ininternal/mcp/client.gobuilds its errors with freshfmt.Errorfwraps (mcp http do: %waround a*url.Error, or a formatted string for a non-2xx), so the match is never true.client.(interface{ Exited() <-chan struct{} })assertion. Only the stdio client implements it, so no watcher is started for HTTP.Observed against a closed
httptestserver after a successful handshake:A server down at initialization is handled correctly — it never goes ready, and
/readyzreports it. The gap is only post-initialization death.Impact
Degraded rather than fatal:
Executorreturns the failure as a tool-result message, so the model sees the error and the request completes. The cost is a wasted round trip per call, and tools advertised that cannot be used.With
mcp_servers[].required: truethe readiness gate is also affected — a required HTTP server that has died reports healthy, because readiness reads the same ready flag that never clears.Why this was not fixed alongside the stdio case
Marking a server unready is terminal. There is no periodic re-probe; recovery requires a configuration reload or a restart. That is correct for a crashed subprocess, which cannot return on its own, but wrong for HTTP: a refused connection is routinely transient — a rolling deploy, a pod restart, a load balancer draining a backend.
Treating the first HTTP error as death would therefore turn a few seconds of redeployment into a permanent withdrawal of that server's tools, and with
required: trueinto a permanent503that takes the instance out of rotation until an operator intervenes. That is worse than the bug.Proposed
Both halves are needed; neither works alone.
Related
AGENTS.mdscopes the post-initialization death-detection guarantee to stdio transports. That note should be removed once this ships.