The pagination loop that ends two pages short

L2WorkingCode reading7 minCodingDebugging

RolesAgent Systems EngineeringDeveloper Experience and Education

The question

A client collects the tool list of an MCP server with this loop:

python
tools = []
cursor = None
while True:
    result = call("tools/list", cursor=cursor)
    tools.extend(result["tools"])
    if not result.get("nextCursor"):
        break
    cursor = result["nextCursor"]

Against one server the client sees only the first page, and the trace shows a single request. Read the code and find the bug. Then name two more things a client must never do with a cursor.

Answer it first, aloud or on paper. Then open the answer.

The answer

Show short answer

The loop tests the cursor for truthiness, and an empty string is a valid cursor. The server returned "" after its first page, the test read it as false, and the loop stopped before the second request. Stop only when nextCursor is absent or null, and send the value back exactly as received. A client must also never decode a cursor or compare two cursors for order.

Show full answer

The one decision a client gets

python
if result.get("nextCursor") is None:
    break
cursor = result["nextCursor"]

The server chooses the page size and the cursor format. The sample server in the lesson deliberately returns "" after its first page, and the correct trace is one request with no cursor followed by one request with the cursor "".

Cursors are opaque

Do not decode a cursor, increment it, compare it with a prior cursor, or infer a page number from it. A server may sign a cursor, bind it to a catalog version, or map it to private state. That is the server's detail and may change without notice. An invalid cursor returns JSON-RPC invalid params, -32602.

Why the bug matters

The tool list is the first gate of the contract pipeline: discover, admit, invoke, execute, consume. A client that drops the last pages exposes a partial catalog to the model and may miss the descriptor it was asked to admit or reject. Discovery must be deterministic and complete before admission starts.

Follow-ups

  1. L2WorkingA page arrives with nextCursor present and an empty tools array. Do you continue?

    Show expected answer

    yes; continue whenever nextCursor is present and not null.

  2. L2WorkingA cursor looks like base64. May the client decode it to skip ahead?

    Show expected answer

    no; the value is opaque, and the server may sign it or bind it to a catalog version.

  3. L3SeniorWhat should a server put in a cursor so that a client cannot misuse it?

    Show expected answer

    a signed or server-mapped token, bound to a catalog version where needed, with an invalid or stale value rejected as -32602.

  4. L4StaffWhat evidence proves complete paginated discovery for a gateway?

    Show expected answer

    the page trace, the admitted and rejected descriptor sets, and a check that repeated calls give the same logical order.

Wrong answers

Show the wrong answers that interviewers hear
  • “An empty string means no more pages.”

    It is a valid cursor. Only absence or null ends the loop.

  • “Parse the cursor to compute the page number.”

    The format belongs to the server and may be signed or private state.

  • “Compare the new cursor with the old one to detect a loop.”

    Clients must not compare cursors for ordering.

Taught in

The tool contracts lesson shows the two-request cursor trace and the opaque-cursor rule.