Walk through resources/list, resources/read, and pagination with cursors.

5 minintermediatemcpresourcespaginationcursor

Quick Answer

resources/list returns the available fixed resources; for large catalogs, the response includes a nextCursor string, and the client passes that back as the cursor param on a follow-up resources/list call to fetch the next page, continuing until no nextCursor is returned. resources/read takes a specific uri and returns a contents array (usually one entry, but a directory-like resource can return multiple) with either text (for text-based content) or blob (base64-encoded binary) per entry, plus a mimeType. Pagination exists specifically so a server with thousands of resources (e.g., every file in a large repo) doesn't have to return them all in one potentially enormous response, and so a client can start acting on the first page while later pages are still being fetched.

Detailed Answer

Pagination in MCP follows a simple cursor pattern, used identically across resources/list, tools/list, and prompts/list.

The listing flow with pagination

// First page
{"method": "resources/list", "params": {}}
// -> {"resources": [...], "nextCursor": "eyJvZmZzZXQiOjEwMH0="}

// Next page
{"method": "resources/list", "params": {"cursor": "eyJvZmZzZXQiOjEwMH0="}}
// -> {"resources": [...], "nextCursor": null}   // no nextCursor => done

The cursor is an opaque string from the client's point of view. It might encode an offset, a database key, anything the server wants. Clients must not try to parse or construct cursors themselves — only pass back exactly what the server last gave them.

Reading a resource

{"method": "resources/read", "params": {"uri": "file:///project/data.csv"}}

// Text content
{"result": {"contents": [
  {"uri": "file:///project/data.csv", "mimeType": "text/csv", "text": "id,name\n1,Alice\n2,Bob"}
]}}

// Binary content
{"result": {"contents": [
  {"uri": "file:///project/logo.png", "mimeType": "image/png", "blob": "iVBORw0KGgoAAAANSUhEUg..."}
]}}

Why cursor-based, not offset/limit

A raw numeric offset breaks if the underlying data set changes between page fetches — items shift, and a delete shifts every subsequent offset. An opaque cursor lets the server encode whatever positioning scheme is actually stable for its data source: a database's own keyset pagination, a stable sort key, or a snapshot ID. None of that implementation detail, or its instability, leaks into the protocol itself.

A common implementation mistake

Treating resources/list as if it always returns everything in one call, and never checking for nextCursor. This works fine in local testing with a handful of files. It silently truncates results the moment a server is pointed at a real, large dataset.

Related Resources