Пагінація
Машинний переклад
Цю сторінку перекладено автоматично з англомовної документації, і основною версією є англомовна сторінка. Якщо щось читається неправильно, на сторінці Переклади пояснено, як про це повідомити.
Більшості серверів вона не знадобиться ніколи.
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: починайте з початку. - Ви вирішуєте, чим курсор є. Тут це зсув, записаний як рядок. Мітка часу, первинний ключ, 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.