fix(everything): prevent URL elicitation error-path infinite loop#4285
Conversation
# Conflicts: # src/everything/docs/structure.md
- Remove src/everything/pnpm-lock.yaml (monorepo uses npm workspaces) - Drop redundant TriggerUrlElicitationSchema.parse(args) in the handler; destructure directly from args like the other tools, since the SDK validates against the registered schema before invoking the handler - Add explicit accept/decline/cancel messaging in the request path, mirroring trigger-elicitation-request.ts - Clarify the registerTool count comment in registrations.test.ts (task-based tools register via registerToolTask, counted separately) - Add tests covering the no-url / undefined-capability registration guards, the randomUUID() elicitationId fallback, and the decline/cancel response paths (100% coverage of the tool) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the annotations block on sibling elicitation tools and the CLAUDE.md guidance. Uses openWorldHint: true since the tool drives an external browser flow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The trigger-url-elicitation tool's error path (errorPath=true) threw UrlElicitationRequiredError (-32042) carrying the same URL that failed and re-threw it on every retry, so clients looped (and tripped their own loop detection). - Point the prerequisite elicitation at a different URL (https://modelcontextprotocol.io) than the failing request. - Track issued prerequisites by the inputs a client resends on retry (session + url + caller-supplied elicitationId); a plain retry is then recognized, ignores errorPath, and proceeds via the request path instead of re-throwing the prerequisite. - Update tests and docs accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@claude review |
|
Claude finished @cliffhall's task in 2m 11s —— View job Review: prevent URL elicitation error-path infinite loopThanks @cliffhall — the core fix is sound and well-documented. The two-part approach (point the prerequisite at a different URL and record a one-shot marker so the retry proceeds via the request path) correctly breaks the loop, and the new test exercises the originating-call → retry sequence. A few things worth addressing, one of them before merge. 🔴 Blocking: literal NUL bytes embedded in the source fileThe diff renders as const errorPathKey = `${sessionId}\x00${url}\x00${requestedElicitationId ?? ""}`;The two separators between Using NUL as a collision-proof delimiter is a fine idea (it can't appear in a URL or session id), but embedding the raw byte in source has real downsides:
Use the escape sequence instead of a literal byte — identical runtime behavior (still a collision-proof delimiter), but the file stays plain text and diffs become reviewable: const errorPathKey = `${sessionId}${url}${requestedElicitationId ?? ""}`;(Or a visible delimiter like 🟡 Unbounded growth of
|
- Replace raw NUL byte delimiters in errorPathKey with the \u0000 escape sequence so the source stays plain text and the diff is reviewable (same collision-proof delimiter at runtime). - Add a one-shot test reset helper + beforeEach so the suite is robust to the module-level marker state regardless of order. - Use a dedicated prerequisite message instead of reusing the caller's. - Document the unbounded-growth tradeoff as a demo simplification and note the one-shot marker semantics in docs/features.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the thorough review — all points addressed in 5e11559. 🔴 NUL bytes (blocking): Fixed. The delimiters were meant to be spaces but ended up as raw NUL bytes during editing, which is why git flagged the file binary and hid the diff. Replaced them with the 🟡 Unbounded growth of 🟡 Module-global state / test isolation: Added an exported 🟢 Nits:
Full |
|
@claude review |
|
Claude finished @cliffhall's task in 1m 38s —— View job Re-review: prevent URL elicitation error-path infinite loopThanks @cliffhall — I re-reviewed ✅ Verified fixes
The new retry test (lines 965–1022) exercises the full sequence well: first call throws Local verification
Optional follow-up (non-blocking)The error-path key incorporates LGTM. 👍 --- · branch |
|
Thanks for the re-review and the local verification — glad it's all confirmed. On the optional follow-up: yes, the split is intentional. An explicit- |
olaservo
left a comment
There was a problem hiding this comment.
Thanks for finding and resolving this!
Description
Fixes an infinite-loop bug in the
everythingserver'strigger-url-elicitationtool error path (errorPath=true).Previously the tool threw
UrlElicitationRequiredError(-32042) carrying the same URL that failed, and re-threw it on every retry. A client would complete the "prerequisite", retry, hit the identical error, and loop — tripping its own loop detection ("The server requested the same URL again after you completed it").Two changes break the loop:
https://modelcontextprotocol.io).sessionId+url+ caller-suppliedelicitationId). When the client satisfies the prerequisite and retries the same call, the retry is recognized, ignoreserrorPath, and proceeds via the normal request path instead of re-throwing.Server Details
trigger-url-elicitation), tests, docsMotivation and Context
Without this, the error-path demonstration of URL elicitation is unusable: any client following the spec's retry-after-prerequisite flow loops forever. This makes the reference behavior correct and demonstrates the intended error-path → satisfy-prerequisite → retry → proceed sequence.
How Has This Been Tested?
src/everything/__tests__/tools.test.ts): the-32042case asserts the prerequisite points at a different URL than the failing request; a new test performs a plain retry (identical args, no echoed id) and asserts the retry takes the request path (elicitation/create) instead of throwing.npm run buildand fullvitestsuite pass (105 tests).Screen capture
elicitation-error-path.mov
Breaking Changes
None. Behavior change is confined to the
errorPath=trueretry flow.Types of changes
Checklist
src/everything/docs/)Additional context
Design note on retry correlation: a real client retries with the original tool-call arguments and does not echo the prerequisite's server-generated
elicitationId. The marker is therefore keyed on stable resent inputs rather than the resolved/randomelicitationId(which would change per call and never match). The marker is one-shot — cleared on the recognized retry — so a fresh attempt re-arms the error path.🤖 Generated with Claude Code