different-ai/openwork · error · ProbeFailure
MCP_TOOL_DISCOVERY
MCP_TOOL_DISCOVERY
Error message
Tool catalog repeated a cursor
What it means
Thrown at the top of the tools/list pagination loop when the cursor about to be sent was already used, i.e. the server handed back a page cursor it previously issued, which would loop forever. The probe keeps a Set of seen cursors and fails fast with an mcp_pagination_loop classification instead of paging indefinitely (the hard cap is 25 pages).
Source
Thrown at packages/enterprise-mcp-mock-server/src/testing/probe.ts:801
}, "MCP_INITIALIZED", overallDeadline)
await discardResponseBody(initializedResponse, "MCP_INITIALIZED", "mcp_lifecycle")
if (initializedResponse.status === 404) {
throw new ProbeFailure("CONTINUITY_SESSION", "mcp_session_expired", "MCP session expired after initialize")
}
if (initializedResponse.status !== 202) {
throw new ProbeFailure("MCP_INITIALIZED", "mcp_lifecycle", `Initialized notification returned HTTP ${initializedResponse.status}`)
}
recordPassed(phases, "MCP_INITIALIZED", startedAt, "Initialized notification received HTTP 202")
startedAt = Date.now()
const toolNames = new Set<string>()
const discoveredTools: MockTool[] = []
const cursors = new Set<string>()
let cursor: string | undefined
let catalogComplete = false
for (let page = 0; page < 25; page += 1) {
if (cursor) {
if (cursors.has(cursor)) throw new ProbeFailure("MCP_TOOL_DISCOVERY", "mcp_pagination_loop", "Tool catalog repeated a cursor")
cursors.add(cursor)
}
const listRawResponse = await fetchStep(mcpUrl, {
method: "POST",
headers: sessionHeaders,
body: JSON.stringify({ jsonrpc: "2.0", id: 10 + page, method: "tools/list", params: cursor ? { cursor } : {} }),
}, "MCP_TOOL_DISCOVERY", overallDeadline)
if (listRawResponse.status === 403) {
const message = await safeHttpErrorMessage(listRawResponse, "PROVIDER_AUTHORIZATION", "provider_per_user_403")
throw new ProbeFailure("PROVIDER_AUTHORIZATION", "provider_per_user_403", `${message} (HTTP 403)`)
}
const listResponse = await expectOk(listRawResponse, "MCP_TOOL_DISCOVERY")
const envelope = await parseRpc(listResponse, "MCP_TOOL_DISCOVERY")
if (envelope.id !== 10 + page) {
throw new ProbeFailure("MCP_TOOL_DISCOVERY", "mcp_tools_discovery", "tools/list response JSON-RPC id did not match the request")
}
if (envelope.error) throw new ProbeFailure("MCP_TOOL_DISCOVERY", "mcp_tools_discovery", envelope.error.message)
const result = parseAt(View on GitHub (pinned to 2b7df46e8a)
Solutions
- Fix the server so each page returns a strictly new nextCursor, omitting it on the final page
- Return nextCursor: undefined/null when no more pages remain
- Test pagination with a tool count that exceeds one page to confirm cursor advancement
Example fix
// before (server)
return { tools: page, nextCursor: encode(pageIndex) } // same cursor when page empty
// after (server)
return hasMore ? { tools: page, nextCursor: encode(pageIndex + 1) } : { tools: page } Defensive patterns
Strategy: validation
Validate before calling
const seen = new Set<string>()
if (cursor && seen.has(cursor)) throw new Error("pagination cursor repeated")
seen.add(cursor) Type guard
function isFreshCursor(c: string | undefined, seen: Set<string>): c is string {
return typeof c === "string" && !seen.has(c)
} Try / catch
try {
await probeEnterpriseMcpMockServer(scenario)
} catch (e) {
if (e instanceof ProbeFailure && e.code === "MCP_TOOL_DISCOVERY") {
console.error("Tool discovery failed:", e.message)
}
} Prevention
- Omit nextCursor on the final page instead of repeating a cursor
- Test pagination with more tools than one page holds
- Generate cursors from monotonically advancing offsets
When it happens
Trigger: During paginated tools/list discovery, the server's nextCursor from page N equals a cursor already issued on an earlier page, so the probe detects a cycle before sending the next request.
Common situations: Server pagination implementation that regenerates cursors deterministically (e.g. cursor always encodes page 0); cursor not advancing after the last full page; server returning the same nextCursor when tool list is unchanged; off-by-one that never advances the offset.
Related errors
- pagination_unsupported
- MCP_CATALOG_CURSOR_LOOP
- invalid_catalog
- MCP_CATALOG_CURSOR_SIZE_LIMIT
- MCP_CATALOG_PAGE_LIMIT
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/721021929c0a4d41.
Report an issue: GitHub.