Перейти до змісту

Пагінація

Машинний переклад

Цю сторінку перекладено автоматично з англомовної документації, і основною версією є англомовна сторінка. Якщо щось читається неправильно, на сторінці Переклади пояснено, як про це повідомити.

Більшості серверів вона не знадобиться ніколи.

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: починайте з початку.
  • Ви вирішуєте, чим курсор є. Тут це зсув, записаний як рядок. Мітка часу, первинний ключ, 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.