Table of contents

How do I implement API pagination with MCP servers?

API pagination is a critical technique when working with MCP (Model Context Protocol) servers, especially when dealing with large datasets that cannot be retrieved in a single request. MCP servers often expose paginated endpoints to efficiently handle data retrieval, and understanding how to implement pagination correctly ensures optimal performance and complete data collection.

Understanding API Pagination in MCP Context

The Model Context Protocol allows AI assistants to interact with external data sources through standardized server implementations. When MCP servers expose APIs that return large datasets—such as search results, database records, or scraped content—pagination becomes essential to manage memory, reduce response times, and comply with rate limits.

There are three primary pagination strategies you'll encounter when working with MCP servers:

  1. Offset-based pagination - Uses offset and limit parameters
  2. Cursor-based pagination - Uses cursor tokens for navigation
  3. Page-based pagination - Uses page numbers and page size

Implementing Offset-Based Pagination

Offset-based pagination is the most straightforward approach, using offset and limit (or skip and take) parameters to control which subset of results to retrieve.

Python Implementation

Here's how to implement offset-based pagination when calling an MCP server API:

import requests
from typing import List, Dict, Any

class MCPPaginatedClient:
    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url
        self.headers = {"Authorization": f"Bearer {api_key}"}

    def fetch_all_records(self, endpoint: str, limit: int = 100) -> List[Dict[str, Any]]:
        """
        Fetch all records from a paginated MCP server endpoint.

        Args:
            endpoint: The API endpoint to query
            limit: Number of records per request

        Returns:
            List of all records from all pages
        """
        all_records = []
        offset = 0

        while True:
            params = {
                "offset": offset,
                "limit": limit
            }

            response = requests.get(
                f"{self.base_url}/{endpoint}",
                headers=self.headers,
                params=params
            )
            response.raise_for_status()

            data = response.json()
            records = data.get("records", [])

            if not records:
                break

            all_records.extend(records)
            offset += limit

            # Check if we've reached the end
            total = data.get("total")
            if total and offset >= total:
                break

        return all_records

# Usage example
client = MCPPaginatedClient(
    base_url="https://api.example.com",
    api_key="your_api_key_here"
)

all_data = client.fetch_all_records("scraped-data", limit=50)
print(f"Retrieved {len(all_data)} total records")

JavaScript Implementation

Here's the equivalent implementation in JavaScript using async/await:

class MCPPaginatedClient {
    constructor(baseUrl, apiKey) {
        this.baseUrl = baseUrl;
        this.apiKey = apiKey;
    }

    async fetchAllRecords(endpoint, limit = 100) {
        const allRecords = [];
        let offset = 0;

        while (true) {
            const url = new URL(`${this.baseUrl}/${endpoint}`);
            url.searchParams.append('offset', offset);
            url.searchParams.append('limit', limit);

            const response = await fetch(url, {
                headers: {
                    'Authorization': `Bearer ${this.apiKey}`
                }
            });

            if (!response.ok) {
                throw new Error(`API error: ${response.status}`);
            }

            const data = await response.json();
            const records = data.records || [];

            if (records.length === 0) {
                break;
            }

            allRecords.push(...records);
            offset += limit;

            // Check if we've reached the end
            if (data.total && offset >= data.total) {
                break;
            }
        }

        return allRecords;
    }
}

// Usage example
const client = new MCPPaginatedClient(
    'https://api.example.com',
    'your_api_key_here'
);

const allData = await client.fetchAllRecords('scraped-data', 50);
console.log(`Retrieved ${allData.length} total records`);

Implementing Cursor-Based Pagination

Cursor-based pagination is more efficient for large datasets and provides consistent results even when data changes between requests. This approach is commonly used by modern APIs, including many MCP server implementations.

Python Implementation with Cursors

class MCPCursorClient:
    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url
        self.headers = {"Authorization": f"Bearer {api_key}"}

    def fetch_all_with_cursor(self, endpoint: str, page_size: int = 100) -> List[Dict[str, Any]]:
        """
        Fetch all records using cursor-based pagination.

        Args:
            endpoint: The API endpoint to query
            page_size: Number of records per page

        Returns:
            List of all records from all pages
        """
        all_records = []
        cursor = None

        while True:
            params = {"page_size": page_size}
            if cursor:
                params["cursor"] = cursor

            response = requests.get(
                f"{self.base_url}/{endpoint}",
                headers=self.headers,
                params=params
            )
            response.raise_for_status()

            data = response.json()
            records = data.get("data", [])

            if not records:
                break

            all_records.extend(records)

            # Get the next cursor
            cursor = data.get("next_cursor")
            if not cursor:
                break

        return all_records

# Usage
client = MCPCursorClient(
    base_url="https://api.example.com",
    api_key="your_api_key_here"
)

results = client.fetch_all_with_cursor("search-results", page_size=100)

JavaScript Cursor Implementation

class MCPCursorClient {
    constructor(baseUrl, apiKey) {
        this.baseUrl = baseUrl;
        this.apiKey = apiKey;
    }

    async fetchAllWithCursor(endpoint, pageSize = 100) {
        const allRecords = [];
        let cursor = null;

        while (true) {
            const url = new URL(`${this.baseUrl}/${endpoint}`);
            url.searchParams.append('page_size', pageSize);
            if (cursor) {
                url.searchParams.append('cursor', cursor);
            }

            const response = await fetch(url, {
                headers: {
                    'Authorization': `Bearer ${this.apiKey}`
                }
            });

            if (!response.ok) {
                throw new Error(`API error: ${response.status}`);
            }

            const data = await response.json();
            const records = data.data || [];

            if (records.length === 0) {
                break;
            }

            allRecords.push(...records);

            // Get the next cursor
            cursor = data.next_cursor;
            if (!cursor) {
                break;
            }
        }

        return allRecords;
    }
}

Implementing Page-Based Pagination

Page-based pagination uses page numbers and is intuitive for developers familiar with traditional web pagination patterns.

Python Page-Based Implementation

def fetch_paginated_data(base_url: str, endpoint: str, api_key: str, per_page: int = 50):
    """Fetch all pages of data from an MCP server endpoint."""
    headers = {"Authorization": f"Bearer {api_key}"}
    all_data = []
    page = 1

    while True:
        params = {
            "page": page,
            "per_page": per_page
        }

        response = requests.get(
            f"{base_url}/{endpoint}",
            headers=headers,
            params=params
        )
        response.raise_for_status()

        data = response.json()
        items = data.get("items", [])

        if not items:
            break

        all_data.extend(items)

        # Check if there are more pages
        if not data.get("has_next_page", False):
            break

        page += 1

    return all_data

Advanced Pagination Techniques

Rate Limiting and Retry Logic

When working with MCP servers, you should implement rate limiting and retry logic to handle API throttling:

import time
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def create_session_with_retries():
    """Create a requests session with automatic retry logic."""
    session = requests.Session()

    retry_strategy = Retry(
        total=3,
        backoff_factor=1,
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=["GET"]
    )

    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("http://", adapter)
    session.mount("https://", adapter)

    return session

class RateLimitedMCPClient:
    def __init__(self, base_url: str, api_key: str, requests_per_second: float = 2):
        self.base_url = base_url
        self.headers = {"Authorization": f"Bearer {api_key}"}
        self.session = create_session_with_retries()
        self.min_interval = 1.0 / requests_per_second
        self.last_request_time = 0

    def _rate_limit(self):
        """Ensure we don't exceed the rate limit."""
        elapsed = time.time() - self.last_request_time
        if elapsed < self.min_interval:
            time.sleep(self.min_interval - elapsed)
        self.last_request_time = time.time()

    def fetch_page(self, endpoint: str, params: dict) -> dict:
        """Fetch a single page with rate limiting."""
        self._rate_limit()

        response = self.session.get(
            f"{self.base_url}/{endpoint}",
            headers=self.headers,
            params=params,
            timeout=30
        )
        response.raise_for_status()
        return response.json()

Parallel Pagination for Known Page Counts

When you know the total number of pages in advance, you can fetch multiple pages in parallel to improve performance:

import asyncio
import aiohttp
from typing import List

async def fetch_page_async(
    session: aiohttp.ClientSession,
    url: str,
    headers: dict,
    page: int,
    per_page: int
) -> List[dict]:
    """Fetch a single page asynchronously."""
    params = {"page": page, "per_page": per_page}

    async with session.get(url, headers=headers, params=params) as response:
        response.raise_for_status()
        data = await response.json()
        return data.get("items", [])

async def fetch_all_pages_parallel(
    base_url: str,
    endpoint: str,
    api_key: str,
    total_pages: int,
    per_page: int = 50
) -> List[dict]:
    """Fetch all pages in parallel."""
    headers = {"Authorization": f"Bearer {api_key}"}
    url = f"{base_url}/{endpoint}"

    async with aiohttp.ClientSession() as session:
        tasks = [
            fetch_page_async(session, url, headers, page, per_page)
            for page in range(1, total_pages + 1)
        ]

        results = await asyncio.gather(*tasks)

        # Flatten the list of lists
        all_items = []
        for items in results:
            all_items.extend(items)

        return all_items

# Usage
all_data = asyncio.run(
    fetch_all_pages_parallel(
        base_url="https://api.example.com",
        endpoint="data",
        api_key="your_api_key",
        total_pages=10
    )
)

Handling Browser-Based Pagination with MCP

When using MCP servers that integrate with browser automation tools like Puppeteer or Playwright, you'll need to handle AJAX requests using Puppeteer to properly wait for paginated content to load. This is particularly important when scraping single-page applications that load data dynamically.

For browser-based pagination, you can combine MCP server capabilities with navigation techniques. Learning how to navigate to different pages using Puppeteer will help you implement pagination in scenarios where clicking "Next" buttons or infinite scroll is required.

Best Practices for MCP Server Pagination

  1. Always implement error handling: API requests can fail due to network issues, rate limits, or server errors. Use try-except blocks and implement retry logic.

  2. Respect rate limits: MCP servers often have rate limits. Implement exponential backoff and respect the Retry-After header when present.

  3. Cache pagination tokens: When using cursor-based pagination, store cursors to enable resuming interrupted requests.

  4. Monitor memory usage: When fetching large datasets, consider processing records in batches rather than loading everything into memory.

  5. Log pagination progress: Track which pages have been fetched to enable debugging and resume functionality.

  6. Use appropriate page sizes: Balance between fewer requests (larger pages) and memory usage (smaller pages). Typical values range from 50 to 1000 records per page.

  7. Handle edge cases: Account for empty results, single-page results, and API changes that might affect pagination structure.

Testing Your Pagination Implementation

Here's a simple test script to verify your pagination logic:

def test_pagination(client, endpoint, expected_total):
    """Test that pagination retrieves all expected records."""
    records = client.fetch_all_records(endpoint)

    assert len(records) == expected_total, \
        f"Expected {expected_total} records, got {len(records)}"

    # Verify no duplicates
    ids = [r['id'] for r in records]
    assert len(ids) == len(set(ids)), "Found duplicate records"

    print(f"✓ Successfully retrieved all {len(records)} records")
    print(f"✓ No duplicates found")

Conclusion

Implementing API pagination with MCP servers requires understanding the pagination strategy used by the server and writing robust code that handles edge cases, rate limits, and errors. Whether you're using offset-based, cursor-based, or page-based pagination, the key principles remain the same: iterate through all pages systematically, handle errors gracefully, and respect API limitations.

By following the patterns and best practices outlined in this guide, you'll be able to efficiently retrieve complete datasets from MCP servers while maintaining good performance and reliability. Remember to always consult the specific MCP server's API documentation for details about its pagination implementation and any special requirements or limitations.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon