Skip to content

Every HttpRequestException on an MCP tool call forces a full reconnect #2056

Description

@Aaronontheweb

Part of #2058. Design: openspec/changes/mcp-tool-outcome-receipts/design.md (D3).

Problem

McpClientManager.IsTransportOrSessionFailure (src/Netclaw.Daemon/Mcp/McpClientManager.cs:1782-1800) returns true for every HttpRequestException. It does not look at StatusCode. An HTTP 500, 429, or 403 on tools/call takes the same path as a dropped socket:

InvokeSharedAsync (:604-612) → ReconnectAfterTransportFailureAsync (:628-660) → ReconnectAsync builds a new HttpClientTransport and McpClient, sends server/discover, initialize, notifications/initialized, and tools/list, and disposes the old client with an HTTP DELETE.

That is about five extra requests per failed call. The generation counter climbs by one each time.

Impact:

  • A new session cannot fix a server-side application error. The reconnect is wasted work.
  • Servers with a request budget get hit harder. short.io allows 30 requests per 60 s across all MCP traffic and returns 429 with a non-JSON-RPC body. One 429 costs about five more requests, so the next call is more likely to get a 429. The loop feeds itself.
  • MCP server '<name>' connected as generation N climbs with no config change. This misleads the operator.

The predicate has a second consumer. McpClientManager.LoadAsync (:541-551, the MCP prompt-skill path) catches McpException when not a transport failure, and any exception when a transport failure. After the predicate changes, an HttpRequestException with an application status would match neither clause and escape to the dispatcher.

Expected

  • IsTransportOrSessionFailure returns true for HttpRequestException only when StatusCode is null or 404 (session expiry per the Streamable HTTP spec). Every other status is an application error. The other branches of the predicate do not change.
  • Tool path: an application error is rethrown without a reconnect. The generation does not change. The tool result names the HTTP status (see MCP tool-call exceptions are recorded as successful tool executions #2055).
  • Prompt path: the first catch clause in LoadAsync widens to McpException or HttpRequestException when not a transport failure. It returns the existing failed load result. No new clause, no new type.
  • Keep the rule: never replay the failed invocation.

Out of scope: Retry-After propagation. HttpRequestException exposes StatusCode only, not headers.

Tests

  • HttpRequestException with StatusCode = 500 or 429: no reconnect, generation unchanged, result names the status.
  • HttpRequestException with StatusCode = 404: one reconnect.
  • HttpRequestException with no status: one reconnect.
  • Prompt load with HttpRequestException StatusCode = 500: failed load result that names the prompt; no reconnect; no exception reaches the dispatcher.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmcpModel context protocol server / client issues.reliabilityRetries, resilience, graceful degradation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions