Paginação
Tradução automática
Esta página foi traduzida automaticamente a partir da documentação em inglês, e a página em inglês é a versão de referência. Se algo parecer errado, Traduções explica como avisar.
A maioria dos servidores nunca precisa disso.
O MCPServer responde a toda requisição list_* com tudo o que tem, em uma única página, next_cursor=None. Para algumas dezenas de ferramentas, recursos ou prompts, essa é a resposta certa e não há nada para configurar.
A paginação é para o servidor cuja lista de recursos é, na verdade, um banco de dados: milhares de linhas que ele se recusa a serializar em uma única resposta. A resposta do protocolo é um cursor: o servidor retorna uma página mais um token opaco, e o cliente envia esse token de volta para obter a próxima página.
O @mcp.resource() não tem nenhum gancho para isso. Para paginar, você mesmo escreve o handler de listagem, no Server de baixo nível.
Um 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)
- Em um
Serverde baixo nível, os handlers são argumentos do construtor, não decoradores. Oon_list_resourcesresponde a toda requisiçãoresources/list; a ligação inteira é essa. - Todo handler paginado é tipado como
params: PaginatedRequestParams | None, e o exemplo aceita os dois. Em uma conexão, porém, o SDK nunca entregaNonepara você (uma requisição sem o membroparamschega ao handler como o modelo com seus valores padrão), então o sinal que importa éparams.cursor is None: comece do início. - Você decide o que um cursor é. Aqui é um offset representado como string. Um timestamp, uma chave primária, um blob em base64: qualquer coisa que você consiga gerar na saída e reconhecer na volta.
next_cursor=Noneé como você diz "essa foi a última página". Não há contagem, nem total, nemhas_more. ONoneé o sinal inteiro.
Tip
Um PAGE_SIZE de 10 deixa o exemplo legível. Escolha o seu por endpoint: uma lista de
recursos de uma linha comporta uma página de 500; uma lista de templates de prompt pesados, não.
O cliente não tem voz nisso, e é assim de propósito.
Experimente
Client(server) se conecta a um Server de baixo nível em memória exatamente como se conecta a um MCPServer.
Chame list_resources() sem argumentos. Você recebe dez recursos, de book-1 a book-10, e next_cursor é a string "10".
Devolva-a com list_resources(cursor="10") e o primeiro recurso é book-11, o novo next_cursor é "20".
A décima página volta com next_cursor definido como None. Pronto.
O loop do cliente
Todo método list_* do Client (list_tools, list_resources, list_resource_templates, list_prompts) aceita a palavra-chave cursor=. Esgotar uma lista paginada é um único 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")
cursorcomeça comoNone, então a primeira requisição não carrega nenhum cursor.- Acumule antes de olhar para
next_cursor: a última página também tem recursos. next_cursor is Noneé a saída. Qualquer outra coisa volta direto paracursor=, intocada.
Execute o main() dele e ele imprime 100 resources: dez páginas de dez, costuradas por um loop que nunca soube que havia dez páginas.
Este é o mesmo loop que O cliente mostra para todo verbo list_*, e ele não custa nada contra um servidor que não pagina: next_cursor é None na primeira resposta e o loop roda uma vez.
As três regras
Cursores são opacos. Um cliente nunca deve interpretar, construir ou adivinhar um. A única fonte legítima de um cursor é o next_cursor da página anterior, literalmente.
O servidor escolhe o tamanho da página. Não existe limit= no protocolo. Se você precisa de um tamanho de página diferente, altere o servidor.
Um cliente que ignora a paginação continua funcionando. Ele chama list_resources() uma vez, recebe os dez primeiros e nunca percebe o next_cursor que jogou fora. Nada quebra; ele só vê menos.
Check
Opaco quer dizer opaco. Invente um cursor (list_resources(cursor="page-2")) e não há
nada que o protocolo possa fazer por você. Este servidor tenta int("page-2"), o handler lança uma exceção,
e o que volta para o cliente é:
MCPError(-32603, 'Internal server error', None)
Um cursor que você não recebeu do servidor é um bug, não um pedido de funcionalidade.
Recapitulando
- O
MCPServerretorna tudo em uma página. A paginação é opcional, e você opta por ela noServerde baixo nível. on_list_resources(eon_list_tools,on_list_prompts,on_list_resource_templates) recebePaginatedRequestParams | None;params.cursoréNonena primeira página.- Você retorna uma página mais
next_cursor: qualquer string que você vá reconhecer depois, ouNonequando não sobrar nada. - O loop do cliente: passe
cursor=, acumule, repita aténext_cursor is None. - Cursores são opacos, o servidor é dono do tamanho da página, e um cliente que não pagina ainda recebe a primeira página.
O restante da API do Server escrita à mão (on_call_tool, dicts input_schema, _meta) está em O Server de baixo nível.