Paginación
Traducción automática
Esta página se tradujo automáticamente a partir de la documentación en inglés, y la página en inglés es la versión de referencia. Si algo no se lee bien, Traducciones explica cómo avisarnos.
La mayoría de los servidores nunca necesitan esto.
MCPServer responde a cada solicitud list_* con todo lo que tiene, en una sola página, next_cursor=None. Para unas cuantas docenas de herramientas, recursos o prompts esa es la respuesta correcta y no hay nada que configurar.
La paginación es para el servidor cuya lista de recursos es en realidad una base de datos: miles de filas que se niega a serializar en una sola respuesta. La respuesta del protocolo es un cursor: el servidor devuelve una página más un token opaco, y el cliente envía ese token de vuelta para obtener la siguiente página.
@mcp.resource() no tiene ningún punto de extensión para nada de eso. Para paginar, escribes el handler de listado tú mismo, sobre el Server de bajo nivel.
Un servidor que pagina
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)
- En un
Serverde bajo nivel, los handlers son argumentos del constructor, no decoradores.on_list_resourcesresponde a cada solicitudresources/list; esa es toda la conexión necesaria. - Todo handler paginado lleva el tipo
params: PaginatedRequestParams | None, y el ejemplo acepta ambos. Sin embargo, a través de una conexión el SDK nunca te entregaNone(una solicitud sin miembroparamsllega al handler como el modelo con sus valores por defecto), así que la señal que importa esparams.cursor is None: empieza desde el principio. - Tú decides qué es un cursor. Aquí es un desplazamiento representado como cadena. Una marca de tiempo, una clave primaria, un blob en base64: cualquier cosa que puedas generar de salida y reconocer cuando vuelva.
next_cursor=Nonees la forma de decir "esa fue la última página". No hay recuento, ni total, nihas_more.Nonees toda la señal.
Tip
Un PAGE_SIZE de 10 hace legible el ejemplo. Elige el tuyo por endpoint: una lista de
recursos de una línea se puede permitir una página de 500; una lista de plantillas de prompt voluminosas, no.
El cliente no tiene voz en ello, y así está diseñado.
Pruébalo
Client(server) se conecta a un Server de bajo nivel en memoria exactamente igual que se conecta a un MCPServer.
Llama a list_resources() sin argumentos. Obtienes diez recursos, de book-1 a book-10, y next_cursor es la cadena "10".
Devuélvelo con list_resources(cursor="10") y el primer recurso es book-11; el nuevo next_cursor es "20".
La décima página vuelve con next_cursor en None. Listo.
El bucle del cliente
Cada método list_* de Client (list_tools, list_resources, list_resource_templates, list_prompts) acepta el argumento nombrado cursor=. Vaciar una lista paginada es un solo 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")
cursorempieza comoNone, así que la primera solicitud no lleva cursor.- Extiende antes de mirar
next_cursor: la última página también tiene recursos. next_cursor is Nonees la salida. Cualquier otra cosa vuelve directamente acursor=, sin tocarla.
Ejecuta su main() e imprime 100 resources: diez páginas de diez, unidas por un bucle que nunca supo que había diez páginas.
Es el mismo bucle que El cliente muestra para cada verbo list_*, y no cuesta nada frente a un servidor que no pagina: next_cursor es None en la primera respuesta y el bucle se ejecuta una vez.
Las tres reglas
Los cursores son opacos. Un cliente nunca debe analizar, construir ni adivinar uno. La única fuente legítima de un cursor es el next_cursor de la página anterior, tal cual.
El servidor elige el tamaño de página. No hay limit= en el protocolo. Si necesitas un tamaño de página distinto, cambias el servidor.
Un cliente que ignora la paginación sigue funcionando. Llama a list_resources() una vez, obtiene los diez primeros y nunca se entera del next_cursor que descartó. Nada se rompe; simplemente ve menos.
Check
Opaco significa opaco. Inventa un cursor (list_resources(cursor="page-2")) y no hay
nada que el protocolo pueda hacer por ti. Este servidor intenta int("page-2"), el handler lanza una excepción,
y lo que le vuelve al cliente es:
MCPError(-32603, 'Internal server error', None)
Un cursor que no obtuviste del servidor es un bug, no una petición de funcionalidad.
Resumen
MCPServerdevuelve todo en una página. La paginación es opcional, y la activas en elServerde bajo nivel.on_list_resources(yon_list_tools,on_list_prompts,on_list_resource_templates) recibePaginatedRequestParams | None;params.cursoresNonepara la primera página.- Devuelves una página más
next_cursor: cualquier cadena que reconozcas después, oNonecuando no queda nada. - El bucle del cliente: pasa
cursor=, acumula, repite hasta quenext_cursor is None. - Los cursores son opacos, el servidor es dueño del tamaño de página y un cliente que no pagina sigue recibiendo la primera página.
El resto de la API del Server escrito a mano (on_call_tool, diccionarios input_schema, _meta) está en El Server de bajo nivel.