fix: bound the salvage walks and mark the scan's refused response (#1909)

Copilot round 6.

Bound both fallback walks with LIST_MAX_PAGES, which is now also passed to
the SDK client as `listMaxPages` so the strict aggregate and its fallback
share one value by construction. A server emitting endlessly UNIQUE cursors
never trips the repeated-cursor guard, so the salvage re-walk and the
SEP-2243 exclusion scan would page and accumulate without bound. Hitting the
cap aborts rather than returning a truncated list as a complete one.

Mark the exclusion scan's refused response on every fallback-failure path.
`salvageList` can rethrow bare because ManagedListState marks from the error
it receives; the scan has no such backstop — listAllTools catches and logs a
failing scan by design — so an unmarked rethrow left a response the client
demonstrably refused rendering as a clean exchange.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDXCSrnSBV7vZfxycG5Z6E
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-15 19:33:27 -04:00
commit 67972adc4e

View file

@ -2,8 +2,10 @@ import { describe, it, expect } from "vitest";
import { z } from "zod/v4";
import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
import {
LIST_MAX_PAGES,
ModernResultEnvelopeSchema,
describeIssues,
listPaginationExceeded,
isClientDecodeRejection,
isSalvageableRejection,
labelForRawItem,
@ -317,4 +319,21 @@ describe("summarizeMalformed", () => {
expect(summary).toContain("index 0");
expect(summary).toContain("(+1 more)");
});
describe("the page bound", () => {
it("matches the cap the SDK client is configured with", () => {
// The value is passed to the SDK as `listMaxPages`, so the strict
// aggregate and the fallback walks are bounded by the same number.
expect(LIST_MAX_PAGES).toBe(64);
});
it("names the method and the cap it hit", () => {
// The walk's failure is logged rather than surfaced (the original
// validation error is what the caller sees), so the message is the only
// record of why salvage gave up.
const error = listPaginationExceeded("resources/templates/list");
expect(error.message).toContain("resources/templates/list");
expect(error.message).toContain("64 pages");
});
});
});