different-ai/openwork · error · ExternalMcpDiagnosticError

MCP_CATALOG_CURSOR_LOOP

MCP_CATALOG_CURSOR_LOOP

Error message

MCP_CATALOG_CURSOR_LOOP

What it means

Thrown when tools/list pagination returns a nextCursor that was already seen in this collection pass, indicating the provider's pagination is looping and will never terminate. The gateway detects this via the seenCursors set and aborts instead of looping forever.

Source

Thrown at ee/apps/den-api/src/capability-sources/external-mcp-client.ts:735

      })
    }
    const cursorMeasurement = measureSerializedJson(
      result.nextCursor,
      EXTERNAL_MCP_CATALOG_LIMIT_BYTES - catalogBytes,
      1,
    )
    if (!cursorMeasurement.ok) {
      throw catalogDiagnosticError({
        tracker: input.diagnostic,
        code: "MCP_CATALOG_BYTE_LIMIT",
        operatorAction: `Reduce the complete serialized tool catalog below ${EXTERNAL_MCP_CATALOG_LIMIT_BYTES} bytes.`,
      })
    }
    catalogBytes += cursorMeasurement.bytes
    if (seenCursors.has(result.nextCursor)) {
      throw catalogDiagnosticError({
        tracker: input.diagnostic,
        code: "MCP_CATALOG_CURSOR_LOOP",
        operatorAction: "Fix the provider's tools/list pagination so each nextCursor advances.",
      })
    }
    seenCursors.add(result.nextCursor)
    cursor = result.nextCursor
  }
  throw catalogDiagnosticError({
    tracker: input.diagnostic,
    code: "MCP_CATALOG_PAGE_LIMIT",
    operatorAction: `Reduce the provider catalog to at most ${pageLimit} pages or use a scoped MCP server.`,
  })
}

export type ExternalMcpConnectResult =
  | { status: "connected" }
  | { status: "needs_auth"; authorizeUrl: string }

/**

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Fix the provider's tools/list so each nextCursor advances to new results
  2. Verify the server actually applies the cursor argument when filtering results
  3. Check for caching layers serving a stale first page for every cursor
  4. If a middle page cursor repeats due to a server bug, report it to the provider operator

Example fix

// before: handler ignores cursor, always returns nextCursor "p:2"
// after: derive nextCursor from the served page, e.g. offset += pageSize
Defensive patterns

Strategy: retry

Validate before calling

const seen = new Set<string>()
let cursor: string | undefined
while (cursor !== undefined) {
  if (seen.has(cursor)) throw new Error("pagination loop detected: repeated nextCursor")
  seen.add(cursor)
  cursor = (await listPage(cursor)).nextCursor
}

Try / catch

try {
  await connectExternalMcp(...)
} catch (err) {
  if (err instanceof Error && err.message.includes("MCP_CATALOG_CURSOR_LOOP")) {
    console.error("Provider pagination loops; fix tools/list nextCursor advancement before retrying")
    // do not blind-retry: the loop is deterministic until the server is fixed
  } else throw err
}

Prevention

When it happens

Trigger: seenCursors.has(result.nextCursor) is true when the next page's cursor is generated — i.e. the provider handed back a cursor identical to one already consumed.

Common situations: Broken server pagination that ignores the cursor and returns page 1's next cursor forever; cursors that encode a filter the server does not actually apply; stale/cached pages; static nextCursor pointing at a page that returns itself.

Related errors


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