Пагинация
Машинный перевод
Эта страница переведена с английской документации автоматически, и основной версией остаётся английская страница. Если что-то читается неправильно, на странице Переводы объясняется, как об этом сообщить.
Большинству серверов это никогда не понадобится.
MCPServer отвечает на каждый запрос list_* всем, что у него есть, одной страницей, с next_cursor=None. Для нескольких десятков инструментов, ресурсов или промптов это правильный ответ, и настраивать здесь нечего.
Пагинация нужна серверу, чей список ресурсов — это по сути база данных: тысячи строк, которые он не станет сериализовать в одном ответе. Протокол предлагает для этого курсор: сервер возвращает страницу и непрозрачный токен, а клиент отправляет этот токен обратно, чтобы получить следующую страницу.
У @mcp.resource() для этого нет никакой точки расширения. Чтобы отдавать данные постранично, обработчик списка пишется вручную — на низкоуровневом Server.
Сервер с пагинацией
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:
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.