Pagination
Traduction automatique
Cette page a été traduite automatiquement à partir de la documentation en anglais, et la page en anglais fait foi. Si quelque chose vous semble incorrect, la page Traductions explique comment le signaler.
La plupart des serveurs n’en ont jamais besoin.
MCPServer répond à chaque requête list_* avec tout ce qu’il a, en une seule page, next_cursor=None. Pour quelques dizaines d’outils, de ressources ou de prompts, c’est la bonne réponse et il n’y a rien à configurer.
La pagination sert au serveur dont la liste de ressources est en réalité une base de données : des milliers de lignes qu’il refuse de sérialiser en une seule réponse. La réponse du protocole est un curseur : le serveur renvoie une page accompagnée d’un jeton opaque, et le client renvoie ce jeton pour obtenir la page suivante.
@mcp.resource() n’offre aucun point d’accroche pour cela. Pour paginer, vous écrivez vous-même le gestionnaire (handler) de liste, sur le Server de bas niveau.
Un serveur qui pagine
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)
- Sur un
Serverde bas niveau, les gestionnaires sont des arguments du constructeur, pas des décorateurs.on_list_resourcesrépond à chaque requêteresources/list; c’est tout le branchement nécessaire. - Chaque gestionnaire paginé est typé
params: PaginatedRequestParams | None, et l’exemple accepte les deux. Sur une connexion, cependant, le SDK ne vous passe jamaisNone(une requête sans membreparamsarrive au gestionnaire sous la forme du modèle avec ses valeurs par défaut), donc le signal qui compte estparams.cursor is None: commencer par le début. - C’est vous qui décidez ce qu’est un curseur. Ici, c’est un décalage (offset) rendu sous forme de chaîne. Un horodatage, une clé primaire, un blob base64 : tout ce que vous pouvez émettre à l’aller et reconnaître au retour.
next_cursor=Noneest votre façon de dire « c’était la dernière page ». Il n’y a ni décompte, ni total, nihas_more.Noneest le signal à lui seul.
Tip
Une valeur de PAGE_SIZE de 10 rend l’exemple lisible. Choisissez la vôtre par point de terminaison : une liste de
ressources d’une ligne peut se permettre une page de 500 ; une liste de gros modèles de prompts, non.
Le client n’a pas son mot à dire, et c’est voulu.
Essayer
Client(server) se connecte à un Server de bas niveau en mémoire exactement comme il se connecte à un MCPServer.
Appelez list_resources() sans argument. Vous obtenez dix ressources, de book-1 à book-10, et next_cursor vaut la chaîne "10".
Renvoyez-la avec list_resources(cursor="10") : la première ressource est book-11, le nouveau next_cursor vaut "20".
La dixième page revient avec next_cursor à None. Terminé.
La boucle côté client
Chaque méthode list_* de Client (list_tools, list_resources, list_resource_templates, list_prompts) accepte un argument nommé cursor=. Vider une liste paginée tient en un 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")
cursordémarre àNone, donc la première requête ne porte aucun curseur.- Étendez la liste avant de regarder
next_cursor: la dernière page contient elle aussi des ressources. next_cursor is Noneest la sortie. Toute autre valeur repart directement danscursor=, telle quelle.
Lancez son main() et il affiche 100 resources : dix pages de dix, assemblées par une boucle qui n’a jamais su qu’il y avait dix pages.
C’est la même boucle que montre Le client pour chaque verbe list_*, et elle ne coûte rien face à un serveur qui ne pagine pas : next_cursor vaut None dès la première réponse et la boucle s’exécute une seule fois.
Les trois règles
Les curseurs sont opaques. Un client ne doit jamais en analyser, en construire ni en deviner un. La seule source légitime d’un curseur est le next_cursor de la page précédente, tel quel.
Le serveur choisit la taille de page. Il n’y a pas de limit= dans le protocole. S’il vous faut une autre taille de page, vous modifiez le serveur.
Un client qui ignore la pagination fonctionne quand même. Il appelle list_resources() une fois, obtient les dix premières, et ne remarque jamais le next_cursor qu’il a jeté. Rien ne casse ; il en voit moins.
Check
Opaque veut dire opaque. Inventez un curseur (list_resources(cursor="page-2")) et le
protocole ne peut rien pour vous. Ce serveur tente int("page-2"), le gestionnaire lève une exception,
et ce qui revient au client est :
MCPError(-32603, 'Internal server error', None)
Un curseur que vous n’avez pas obtenu du serveur est un bogue, pas une demande de fonctionnalité.
Récapitulatif
MCPServerrenvoie tout en une seule page. La pagination est facultative, et vous l’activez sur leServerde bas niveau.on_list_resources(ainsi queon_list_tools,on_list_prompts,on_list_resource_templates) reçoitPaginatedRequestParams | None;params.cursorvautNonepour la première page.- Vous renvoyez une page plus un
next_cursor: n’importe quelle chaîne que vous reconnaîtrez plus tard, ouNonequand il ne reste rien. - La boucle côté client : passez
cursor=, accumulez, répétez jusqu’à ce quenext_cursor is None. - Les curseurs sont opaques, la taille de page appartient au serveur, et un client qui ne pagine pas obtient quand même la première page.
Le reste de l’API Server écrite à la main (on_call_tool, les dicts input_schema, _meta) se trouve dans Le Server de bas niveau.