Repository navigation
mcpx: re-issue a broken modern request once, tools/call gated on annotations (#1638) - #1645
Conversation
…tations (#1638) lost() stamps its synthesized -32000 with a mcpxStreamLost data marker; Client.roundTrip re-issues a marked stream-lost exactly once with a fresh id on modern connections only (legacy keeps resume()). Unmarked -32000 (receive-loop connection-closed, server-sent) is never re-issued and no message matching is used. tools/call re-issues only when the tool retained list-time annotations declare readOnlyHint or idempotentHint; hints are retained in ListTools in every era/transport (not gated on mirrorsHeaders). Retires the streamable-http-client-reissue-broken-request conformance gap and regenerates the matrix.
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 2 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c2b26d2d7e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if attempt == 0 { | ||
| // After toolCallHeaders: on a modern HTTP connection it | ||
| // has just listed, so the retained hints are current. | ||
| allowReissue = c.toolCallReissueAllowed(name) | ||
| } |
There was a problem hiding this comment.
Recompute retry eligibility after refreshing tool annotations
On modern HTTP, a HeaderMismatch causes the next iteration's toolCallHeaders(..., true) to refresh ListTools, which replaces the retained tool hints. However, allowReissue remains frozen at its initial value. If the refreshed definition removes readOnlyHint or idempotentHint, a subsequent broken response still reissues the now-unsafe tool using the stale permission, potentially executing its effects twice. Recompute eligibility after each toolCallHeaders call so it reflects the refreshed definition.
Useful? React with 👍 / 👎.
| func (c *Client) call(ctx context.Context, method string, params json.RawMessage, out any) error { | ||
| return c.callReissue(ctx, method, params, out, true) |
There was a problem hiding this comment.
Apply the tool retry gate to generic requests
When a caller uses Client.Request(ctx, "tools/call", params) instead of the typed helper, request.go routes directly through c.call, which now unconditionally enables reissue. A broken modern HTTP response therefore repeats even a destructive or unannotated tool, bypassing the safety gate in CallTool and potentially duplicating side effects. Apply the tool-specific policy to this generic entry point as well, or conservatively disable reissue for generic tools/call requests.
Useful? React with 👍 / 👎.
…reissue # Conflicts: # pkgs/mcpx/docs/spec/conformance-matrix.md # pkgs/mcpx/internal/mcpclient/client.go
…reissue # Conflicts: # pkgs/mcpx/docs/spec/conformance-matrix.md # pkgs/mcpx/internal/mcpclient/client.go
|
Merged current main ( |
…1639) (#1764) * docs(mcpx): register truth-up TR-29 / PM-06 as-built + roadmap row (#1639) Phase 3 of design #1634, per triage 1772. TR-29 now describes the landed Phase 1 client-layer session-404 recovery (#1637, PR #1642) instead of mcpx missing; PM-06 describes the landed Phase 2 broken-stream re-issue (#1638, PR #1645) with the tools/call annotation gate recorded as mcpx's proxy decision, instead of not examined; roadmap Legacy client robustness row takes the triage-settled wording (status stays proposed). Docs only. * style(mcpx): treefmt (prettier) the #1639 register truth-up docs
Implements #1638 (Phase 2 of #1634): a modern request whose response stream breaks unanswered is re-issued exactly once with a new id, per the 2026-07-28 transport MUST.
Stream-lost identity is a data marker, not the code or the message.
lost()(http.go) now stamps its synthesizedcodeStreamLost(-32000) error withdata: {"mcpxStreamLost": true}.isStreamLost(client.go) requires code + marker: the receive loop's own -32000 "connection closed" and any server-sent unmarked -32000 are never re-issued, and there is no message matching anywhere.Re-issue lives in
Client.roundTripas an attempt loop overroundTripOnce: on a marked stream-lost, when policy allows andctx.Err() == nil, it re-sends once with a fresh id fromnextID; a second marked failure surfaces. Era-gated toEraModern— legacy keepsresume(). Each MRTR round is its ownroundTripcall, so each round re-issues independently. Cancellation is never re-issued (guarded both in the loop and inroundTripOnce, where a cancelled caller hears its own cancellation).tools/call is annotation-gated.
call/callReissuecarry the caller's policy: true for every method excepttools/call, whereCallToolcomputes it from retained list-time annotations — re-issue only when the tool declaredreadOnlyHintoridempotentHint. Retention (recordToolHints, called fromListTools) follows thetoolHeadersprecedent but is NOT gated onmirrorsHeaders(), so the gate works on every transport. The gate reads only retained state (no lazytools/listround trip inCallTool); on modern HTTP,toolCallHeaderslists before the first attempt, so the gate computed after it is current.Conformance: gap
streamable-http-client-reissue-broken-requestretired, covered byTestModernReissueBrokenRequest; matrix regenerated.Tests (new
reissue_test.go, all passing): marked re-issue success with fresh id; second stream-lost surfaces; unmarked -32000 not re-issued (receive-loop shape, server-sent same-message, server-sent other-data); cancellation not re-issued; legacy era untouched; annotation gate (readOnly/idempotent re-issued, destructive/unannotated not). Fullpkgs/mcpxpackages verified on the laptop: mcpclient, conformance, daemon and all remaining packages pass (e2e is long-running locally; CI is the arbiter).Queue task 1778. Refs #1634. Phase 1 (#1637, legacy) and Phase 3 (#1639) are out of scope, as scoped in the issue.
🤖 Agent note · queue worker (repo-worker-code), lane B · task 1778
Need help on this PR? Tag
@codesmith-botwith what you need.