Follow-up hardening: MCP init retry, safe HTTP retry semantics, MCP server protocol compliance#28
Merged
Merged
Conversation
The initialize handshake ran under a sync.Once that stored the first error forever: if the first scenario's deadline expired mid-handshake or the server was briefly restarting, every remaining scenario in the run failed instantly with the stale error. A failed attempt is no longer cached — the next Invoke retries, with a mutex preserving Once's serialization of concurrent first invokes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two fixes to the shared HTTP retry loop (used by the HTTP, OpenAI, and Anthropic adapters): - When a 429/503's Retry-After exceeded the remaining context budget, the response body was closed before being returned, so callers got 'read on closed response body' instead of the actual rate-limit response. The body is now closed only after committing to another attempt — and drained first so the keep-alive connection is reused instead of forcing a new TCP+TLS handshake against an already-degraded server. - Transport errors were retried for every method, but a connection can die after a request was fully delivered — replaying a POST there risks duplicate writes or double charges. Transport-error retries are now limited to idempotent methods (GET/HEAD/OPTIONS); 429/503 responses still retry for all methods since the server explicitly rejected the request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tool results Three protocol-compliance fixes for the stdio MCP server: - Request ids are kept as raw JSON so id: 0 and id: "" round-trip exactly (omitempty previously dropped them, breaking client correlation), and parse errors now carry the spec-required id: null. - The final request of a session without a trailing newline (common for one-shot piped clients) is processed at EOF instead of silently dropped. - Ordinary tool execution failures return isError results the calling model can read and self-correct from; unknown tools are -32602 and contained panics remain -32603, instead of every failure surfacing as a blanket internal error that some clients treat as a broken server. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #27, fixing the remaining medium-severity items from the reliability audit. Stacked on
improve— retarget tomain(or let GitHub auto-retarget) once #27 merges.MCP adapter: transient init failure no longer poisons the run (
cleanr/adapters/mcp.go)The initialize handshake ran under a
sync.Oncethat cached the first error forever — one expired deadline or a briefly-restarting server made every remaining scenario fail instantly with the stale error. Failed attempts now retry on the nextInvoke; a mutex preserves the old serialization of concurrent first invokes.Safe retry semantics in
doWithRetry(cleanr/adapters/http.go, shared by HTTP/OpenAI/Anthropic adapters)Retry-Afterexceeded the remaining context budget, the response was returned with an already-closed body — callers sawread on closed response bodyinstead of the actual rate-limit response. The body is now closed only after committing to another attempt, and drained first so the keep-alive connection is reused rather than re-handshaking against an already-degraded server.MCP server protocol compliance (
internal/mcpserver)id: 0/id: ""round-trip exactly (omitempty silently dropped them, breaking client correlation), and parse errors carry the spec-requiredid: null.isError: trueresults the calling model can read and self-correct from; unknown tools are-32602; contained panics remain-32603. Previously every failure was a blanket-32603, which some clients treat as a broken server.Behavior changes to be aware of
-32603for argument errors will now seeisErrortool results (spec-conformant).Test plan
cleanr-dev check(gofiles, fmt, vet,go test -race -shuffle=on ./...) — 431 passed, 0 failedid: nullon parse error; isError vs -32602 vs -32603 failure modes🤖 Generated with Claude Code