Paginierung
Maschinelle Übersetzung
Diese Seite wurde automatisch aus der englischen Dokumentation übersetzt, und die englische Seite ist die maßgebliche Fassung. Wenn sich etwas falsch liest, erklärt Übersetzungen, wie du es melden kannst.
Die meisten Server brauchen das nie.
MCPServer beantwortet jeden list_*-Request mit allem, was er hat, auf einer Seite, next_cursor=None. Bei ein paar Dutzend Tools, Ressourcen oder Prompts ist das die richtige Antwort, und es gibt nichts zu konfigurieren.
Paginierung ist für den Server gedacht, dessen Ressourcenliste in Wahrheit eine Datenbank ist: Tausende Zeilen, die er nicht in einer einzigen Response serialisieren will. Die Antwort des Protokolls darauf ist ein Cursor: Der Server gibt eine Seite plus ein opakes Token zurück, und der Client schickt dieses Token zurück, um die nächste Seite zu bekommen.
@mcp.resource() hat dafür keinen Einstiegspunkt. Um seitenweise auszuliefern, schreibst du den List-Handler selbst, auf dem Low-Level-Server.
Ein Server, der paginiert
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)
- Auf einem Low-Level-
Serversind Handler Konstruktorargumente, keine Dekoratoren.on_list_resourcesbeantwortet jedenresources/list-Request; mehr Verkabelung gibt es nicht. - Jeder paginierte Handler ist als
params: PaginatedRequestParams | Nonetypisiert, und das Beispiel akzeptiert beides. Über eine Verbindung übergibt dir das SDK jedoch nieNone(ein Request ohneparams-Member erreicht den Handler als Modell mit seinen Standardwerten). Das Signal, auf das es ankommt, ist daherparams.cursor is None: von vorne beginnen. - Du entscheidest, was ein Cursor ist. Hier ist es ein Offset, als String dargestellt. Ein Zeitstempel, ein Primärschlüssel, ein Base64-Blob: alles, was du beim Herausgeben erzeugen und beim Zurückkommen wiedererkennen kannst.
- Mit
next_cursor=Nonesagst du „das war die letzte Seite“. Es gibt keine Anzahl, keine Gesamtsumme, keinhas_more.Noneist das ganze Signal.
Tip
Eine PAGE_SIZE von 10 macht das Beispiel lesbar. Wähle deine pro Endpunkt: Eine Liste
einzeiliger Ressourcen verträgt eine Seite mit 500 Einträgen; eine Liste fetter Prompt-Templates nicht.
Der Client hat dabei nichts mitzureden, und das ist Absicht.
Ausprobieren
Client(server) verbindet sich im Speicher mit einem Low-Level-Server genau so, wie er sich mit einem MCPServer verbindet.
Rufe list_resources() ohne Argumente auf. Du bekommst zehn Ressourcen, book-1 bis book-10, und next_cursor ist der String "10".
Gib ihn mit list_resources(cursor="10") zurück, und die erste Ressource ist book-11, der neue next_cursor ist "20".
Die zehnte Seite kommt mit next_cursor auf None zurück. Fertig.
Die Client-Schleife
Jede list_*-Methode auf Client (list_tools, list_resources, list_resource_templates, list_prompts) nimmt ein Keyword-Argument cursor=. Eine paginierte Liste leerzulesen ist ein einziges 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")
cursorbeginnt alsNone, der erste Request trägt also keinen Cursor.- Erweitere die Liste, bevor du auf
next_cursorschaust: Auch die letzte Seite enthält Ressourcen. next_cursor is Noneist der Ausstieg. Alles andere geht unverändert direkt zurück incursor=.
Führe sein main() aus, und es gibt 100 resources aus: zehn Seiten zu je zehn, zusammengefügt von einer Schleife, die nie wusste, dass es zehn Seiten waren.
Das ist dieselbe Schleife, die Der Client für jedes list_*-Verb zeigt, und sie kostet nichts gegenüber einem Server, der nicht paginiert: next_cursor ist schon in der ersten Response None, und die Schleife läuft genau einmal.
Die drei Regeln
Cursor sind opak. Ein Client darf einen Cursor nie parsen, bauen oder erraten. Die einzige zulässige Quelle eines Cursors ist der next_cursor der vorherigen Seite, wortwörtlich.
Der Server bestimmt die Seitengröße. Es gibt kein limit= im Protokoll. Wenn du eine andere Seitengröße brauchst, änderst du den Server.
Ein Client, der Paginierung ignoriert, funktioniert trotzdem. Er ruft list_resources() einmal auf, bekommt die ersten zehn und bemerkt den next_cursor, den er weggeworfen hat, nie. Nichts geht kaputt; er sieht nur weniger.
Check
Opak heißt opak. Erfinde einen Cursor (list_resources(cursor="page-2")), und das
Protokoll kann nichts für dich tun. Dieser Server versucht int("page-2"), der Handler löst eine Exception aus,
und beim Client kommt an:
MCPError(-32603, 'Internal server error', None)
Ein Cursor, den du nicht vom Server bekommen hast, ist ein Bug, kein Feature-Wunsch.
Zusammenfassung
MCPServergibt alles auf einer Seite zurück. Paginierung ist optional, und du aktivierst sie auf dem Low-Level-Server.on_list_resources(undon_list_tools,on_list_prompts,on_list_resource_templates) erhältPaginatedRequestParams | None;params.cursorist bei der ersten SeiteNone.- Du gibst eine Seite plus
next_cursorzurück: einen beliebigen String, den du später wiedererkennst, oderNone, wenn nichts mehr übrig ist. - Die Client-Schleife:
cursor=übergeben, sammeln, wiederholen, bisnext_cursor is None. - Cursor sind opak, die Seitengröße gehört dem Server, und ein Client ohne Paginierung bekommt trotzdem Seite eins.
Der Rest der handgeschriebenen Server-API (on_call_tool, input_schema-Dicts, _meta) steht in Der Low-Level-Server.