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

  1. Fix the server so each page returns a strictly new nextCursor, omitting it on the final page
  2. Return nextCursor: undefined/null when no more pages remain
  3. 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

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


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/721021929c0a4d41. Report an issue: GitHub.