A tool that finds 10,000 records should not return 10,000 records in one call. The database may handle it, but the agent still has to receive, read, and reason over the result. That costs context and makes the useful matches harder to spot. A better contract returns a bounded page plus a way to request the next one.

This guide is about paginating your tool’s search results. MCP also defines cursor pagination for protocol list operations, such as listing tools or resources. That mechanism does not automatically paginate the data returned by an arbitrary tools/call. For a search tool, the query arguments and result shape are yours to design.

Give the agent an explicit page contract

A useful search tool accepts query, an optional cursor, and a bounded limit. It returns a small items array and either a next_cursor or null. The description should tell the agent that it can stop after the first page and request another only if needed.

For example, a document-search response might look like this:

{
  "items": [
    {"id": "doc-104", "title": "On-call checklist", "snippet": "Check the service dashboard first."},
    {"id": "doc-118", "title": "Escalation policy", "snippet": "Page the primary owner after triage."}
  ],
  "next_cursor": "opaque-token-from-server"
}

The token is an illustration, not a value clients should parse. Keep it opaque so you can change how paging works without changing the tool interface. Don’t return the entire document body in search results. Let a separate detail tool fetch the selected record by ID. The same separation works for issues, customer records, log entries, and repository files.

Implement keyset paging in the data layer

For a stable sort by created_at and id, fetch one more row than you intend to return. The extra row tells you whether another page exists. A conceptual SQL query is:

SELECT id, title, snippet, created_at
FROM documents
WHERE tenant_id = :tenant_id
  AND search_vector @@ websearch_to_tsquery(:query)
  AND (
    :after_created_at IS NULL
    OR (created_at, id) < (:after_created_at, :after_id)
  )
ORDER BY created_at DESC, id DESC
LIMIT :limit_plus_one;

This is a PostgreSQL-style sketch, not a drop-in query for every database. The first page has no after values. Return at most limit rows; if an extra row exists, build the next cursor from the last returned row. The id tie-breaker prevents records sharing a timestamp from being skipped or repeated just because their order is ambiguous. Use a normal indexed ordering and test its performance against your real search predicate.

If you rank by relevance instead, the cursor must capture every ordering field and a deterministic tie-breaker. Changes to the underlying index or records between calls can still cause duplicates or omissions. For workflows that require a perfectly consistent export, use a database snapshot or export job rather than pretending a live search is immutable.

Bind the cursor to its scope

A cursor is not a permission grant. On every call, authenticate the caller and apply the tenant and record-level filters again. Bind cursor state to the query, sort order, and tenant. If any of those differ from the original request, reject the cursor and ask the client to restart at page one. Add an expiry if cursors are long-lived enough to go stale.

You can store cursor state server-side behind a random token, or encode it and protect it against tampering with an authenticated signature. Do not trust a client-supplied tenant_id inside a decoded cursor. Derive authorization scope from the authenticated request. Avoid putting secrets or sensitive search terms into a plain base64 token: encoding is not encryption.

Also cap limit on the server. A suggested default of 20 and maximum of 50 are design choices, not MCP protocol requirements. Your actual cap should reflect item size, latency, and what a human or agent can use in one step. A response with 50 full documents may still be much too large.

Expose a useful result, not a hidden loop

A tool named search_documents could describe its arguments this way:

  • query (required): search terms.
  • limit (optional): maximum items in this response, subject to the server cap.
  • cursor (optional): opaque token from a previous response for the same query.

Return items and next_cursor consistently, including when there are no matches. If your SDK supports typed tool output, declare an output schema so clients can inspect those fields. MCP tool results can include structuredContent; consult the SDK version you use for how to produce it. If your client consumes text instead, serialize a concise JSON object rather than a wall of prose. Don’t claim the protocol’s nextCursor field from list responses is automatically present in your search tool result. Your next_cursor is part of your tool’s returned data.

Let the agent decide whether to continue. A client may only need the top three matches before calling get_document. Automatically draining every page inside the tool defeats the reason for pagination. When a caller does request another page, it should pass the original query and the returned cursor together.

Test the edges before connecting an agent

Use the MCP Inspector and local debugging workflow to call the tool with an empty result, one full page, and more than one page. Check that the last page returns next_cursor: null. Then change the query while reusing a cursor, tamper with its bytes, and try a cursor under another user or tenant. Those requests must not disclose cross-scope records. Test insertions between pages and document the consistency your server actually offers.

If you are still shaping the input contract, validate MCP tool inputs before the search executes. If you’re deciding whether your dataset belongs behind a search tool at all, compare MCP resources and resource templates: known read-only content may be easier to expose as resources, while filtered, computed search fits a tool. The first Python server walkthrough covers registration and local setup.

FAQ

Does MCP paginate every tool response for me? No. The protocol specifies cursor-based pagination for certain list methods. Pagination within a custom tool’s search result is an application-level contract you implement.

Should the cursor include the page number? It can in a simple implementation, but keyset cursors usually behave better when a dataset changes or offsets get expensive. Treat the token as opaque either way.

What if the agent needs every record? Don’t turn an interactive search call into an unbounded export. Offer a separate, authorized bulk-export workflow with an explicit output destination, size limits, and audit trail.