Перейти к содержанию

Пагинация

Машинный перевод

Эта страница переведена с английской документации автоматически, и основной версией остаётся английская страница. Если что-то читается неправильно, на странице Переводы объясняется, как об этом сообщить.

Большинству серверов это никогда не понадобится.

MCPServer отвечает на каждый запрос list_* всем, что у него есть, одной страницей, с next_cursor=None. Для нескольких десятков инструментов, ресурсов или промптов это правильный ответ, и настраивать здесь нечего.

Пагинация нужна серверу, чей список ресурсов — это по сути база данных: тысячи строк, которые он не станет сериализовать в одном ответе. Протокол предлагает для этого курсор: сервер возвращает страницу и непрозрачный токен, а клиент отправляет этот токен обратно, чтобы получить следующую страницу.

У @mcp.resource() для этого нет никакой точки расширения. Чтобы отдавать данные постранично, обработчик списка пишется вручную — на низкоуровневом Server.

Сервер с пагинацией

server.py
from typing import Any

from mcp.server import Server, ServerRequestContext
from mcp.types import ListResourcesResult, PaginatedRequestParams, Resource

BOOKS = [f"book-{n}" for n in range(1, 101)]

PAGE_SIZE = 10


async def list_books(ctx: ServerRequestContext[Any], params: PaginatedRequestParams | None) -> ListResourcesResult:
    start = 0 if params is None or params.cursor is None else int(params.cursor)
    end = start + PAGE_SIZE
    page = [Resource(uri=f"books://catalog/{name}", name=name) for name in BOOKS[start:end]]
    next_cursor = str(end) if end < len(BOOKS) else None
    return ListResourcesResult(resources=page, next_cursor=next_cursor)


server = Server("Bookshop", on_list_resources=list_books)
  • На низкоуровневом Server обработчики — это аргументы конструктора, а не декораторы. on_list_resources отвечает на каждый запрос resources/list; вот и всё подключение.
  • Каждый обработчик с пагинацией имеет тип параметра params: PaginatedRequestParams | None, и пример принимает оба варианта. Однако при работе через подключение SDK никогда не передаёт None (запрос без поля params доходит до обработчика как модель со значениями по умолчанию), поэтому значимый сигнал — это params.cursor is None: начать с начала.
  • Что такое курсор, решаете вы. Здесь это смещение, записанное строкой. Временная метка, первичный ключ, blob в base64 — всё, что можно выдать на выходе и распознать на обратном пути.
  • next_cursor=None — это способ сказать «это была последняя страница». Нет ни счётчика, ни общего количества, ни has_more. None — и есть весь сигнал.

Tip

PAGE_SIZE, равный 10, делает пример читаемым. Свой размер выбирайте отдельно для каждой конечной точки: список однострочных ресурсов может позволить себе страницу на 500 элементов; список объёмных шаблонов промптов — нет. Клиент на это никак не влияет, и так задумано.

Попробуйте сами

Client(server) подключается к низкоуровневому Server в памяти точно так же, как к MCPServer.

Вызовите list_resources() без аргументов. Придут десять ресурсов, от book-1 до book-10, а next_cursor будет строкой "10".

Передайте его обратно через list_resources(cursor="10") — первым ресурсом окажется book-11, а новый next_cursor будет "20".

Десятая страница приходит с next_cursor, равным None. Готово.

Цикл на стороне клиента

Каждый метод list_* у Client (list_tools, list_resources, list_resource_templates, list_prompts) принимает именованный аргумент cursor=. Вычитать список постранично целиком — это один while True:

client.py
from typing import Any

from mcp import Client
from mcp.server import Server, ServerRequestContext
from mcp.types import ListResourcesResult, PaginatedRequestParams, Resource

BOOKS = [f"book-{n}" for n in range(1, 101)]

PAGE_SIZE = 10


async def list_books(ctx: ServerRequestContext[Any], params: PaginatedRequestParams | None) -> ListResourcesResult:
    start = 0 if params is None or params.cursor is None else int(params.cursor)
    end = start + PAGE_SIZE
    page = [Resource(uri=f"books://catalog/{name}", name=name) for name in BOOKS[start:end]]
    next_cursor = str(end) if end < len(BOOKS) else None
    return ListResourcesResult(resources=page, next_cursor=next_cursor)


server = Server("Bookshop", on_list_resources=list_books)


async def main() -> None:
    async with Client(server) as client:
        resources: list[Resource] = []
        cursor: str | None = None
        while True:
            page = await client.list_resources(cursor=cursor)
            resources.extend(page.resources)
            if page.next_cursor is None:
                break
            cursor = page.next_cursor
        print(f"{len(resources)} resources")
  • cursor начинается с None, поэтому первый запрос уходит без курсора.
  • Добавляйте элементы до того, как смотреть на next_cursor: на последней странице тоже есть ресурсы.
  • next_cursor is None — условие выхода. Всё остальное без изменений отправляется обратно в cursor=.

Запустите его main(), и он напечатает 100 resources: десять страниц по десять, сшитых циклом, который и не знал, что страниц было десять.

Это тот же цикл, который Клиент показывает для каждого метода list_*, и против сервера без пагинации он ничего не стоит: next_cursor равен None уже в первом ответе, и цикл выполняется один раз.

Три правила

Курсоры непрозрачны. Клиент никогда не должен разбирать, собирать или угадывать курсор. Единственный законный источник курсора — next_cursor предыдущей страницы, дословно.

Размер страницы выбирает сервер. В протоколе нет limit=. Если нужен другой размер страницы, меняется сервер.

Клиент, игнорирующий пагинацию, всё равно работает. Он вызывает list_resources() один раз, получает первые десять и не замечает выброшенный next_cursor. Ничего не ломается; он просто видит меньше.

Check

Непрозрачный значит непрозрачный. Придумайте курсор (list_resources(cursor="page-2")) — и протокол ничем не сможет помочь. Этот сервер пробует int("page-2"), обработчик выбрасывает исключение, и клиенту приходит:

MCPError(-32603, 'Internal server error', None)

Курсор, полученный не от сервера, — это ошибка, а не запрос на новую возможность.

Итоги

  • MCPServer возвращает всё одной страницей. Пагинация включается по желанию, и включается она на низкоуровневом Server.
  • on_list_resources (а также on_list_tools, on_list_prompts, on_list_resource_templates) получает PaginatedRequestParams | None; для первой страницы params.cursor равен None.
  • Вы возвращаете страницу и next_cursor: любую строку, которую потом узнаете, или None, когда больше ничего не осталось.
  • Цикл клиента: передать cursor=, накопить, повторять, пока не next_cursor is None.
  • Курсоры непрозрачны, размер страницы — за сервером, а клиент без пагинации всё равно получает первую страницу.

Остальной API Server, который пишется вручную (on_call_tool, словари input_schema, _meta), — на странице Низкоуровневый Server.