コンテンツにスキップ

ページネーション

機械翻訳

このページは英語版ドキュメントから自動翻訳されたものであり、正式な版は英語版のページです。不自然な箇所があれば、翻訳についてで報告の方法を説明しています。

ほとんどのサーバーには必要ありません。

MCPServer はすべての list_* リクエストに対して、持っているものを全部 1 ページにまとめ、next_cursor=None で返します。ツールやリソース、プロンプトが数十個程度なら、それが正しい答えであり、設定することは何もありません。

ページネーションは、リソース一覧が実質的にデータベースであるようなサーバーのためのものです。数千行もあり、1 つのレスポンスにシリアライズするわけにはいかない場合です。プロトコルの答えはカーソルです。サーバーはページと不透明なトークンを返し、クライアントはそのトークンを送り返して次のページを取得します。

@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 にしているのは、例を読みやすくするためです。実際の値はエンドポイントごとに選んでください。1 行のリソースが並ぶ一覧なら 1 ページ 500 件でも問題ありませんが、大きなプロンプトテンプレートの一覧ではそうはいきません。クライアントに選択の余地はなく、それは意図された設計です。

試してみる

Client(server) は、MCPServer に接続するのとまったく同じように、低レベルの Server にメモリ内で接続します。

引数なしで list_resources() を呼び出してください。book-1 から book-10 までの 10 個のリソースが返り、next_cursor は文字列 "10" です。

それを list_resources(cursor="10") として返すと、最初のリソースは book-11 になり、新しい next_cursor"20" です。

10 ページ目は next_cursorNone に設定されて返ってきます。これで完了です。

クライアントのループ

Client のすべての list_* メソッド(list_toolslist_resourceslist_resource_templateslist_prompts)は cursor= キーワードを受け取ります。ページングされた一覧をすべて取り出すには、while True を 1 つ書くだけです。

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")
  • cursorNone から始まるので、最初のリクエストにはカーソルがありません。
  • next_cursor を見る前に extend してください。最後のページにもリソースはあります。
  • next_cursor is None が出口です。それ以外はそのまま、手を加えずに cursor= に戻します。

その main() を実行すると 100 resources と表示されます。10 件ずつの 10 ページが、10 ページあることなど知らないループによってつなぎ合わされた結果です。

これは クライアント がすべての list_* メソッドについて示しているのと同じループで、ページングしないサーバーに対してもコストはかかりません。最初のレスポンスで next_cursorNone になり、ループは 1 回だけ回ります。

3 つのルール

カーソルは不透明です。 クライアントはカーソルを解析したり、組み立てたり、推測したりしてはいけません。カーソルの正当な出どころは、前のページの next_cursor をそのまま使うことだけです。

ページサイズはサーバーが決めます。 プロトコルに limit= はありません。別のページサイズが必要なら、サーバーを変更します。

ページングを無視するクライアントもそのまま動きます。 list_resources() を 1 回呼び、最初の 10 件を受け取り、捨ててしまった next_cursor に気づくことはありません。何も壊れません。見えるものが少ないだけです。

Check

不透明とは本当に不透明ということです。カーソルをでっち上げても(list_resources(cursor="page-2"))、プロトコルにできることは何もありません。このサーバーは int("page-2") を試み、ハンドラーが例外を送出し、クライアントに返ってくるのは次のとおりです。

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

サーバーから受け取ったものではないカーソルはバグであり、機能要望ではありません。

まとめ

  • MCPServer はすべてを 1 ページで返します。ページネーションはオプトインであり、低レベルの Server でオプトインします。
  • on_list_resources(および on_list_toolson_list_promptson_list_resource_templates)は PaginatedRequestParams | None を受け取ります。最初のページでは params.cursorNone です。
  • ページと next_cursor を返します。後で認識できる任意の文字列か、残りが何もないときは None です。
  • クライアントのループは、cursor= を渡し、蓄積し、next_cursor is None になるまで繰り返します。
  • カーソルは不透明で、ページサイズはサーバーが決め、ページングしないクライアントも 1 ページ目は受け取れます。

手書きの Server API の残り(on_call_toolinput_schema の dict、_meta)は 低レベルの Server にあります。