Multi-Roundtrip-Requests
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.
Manchmal kann ein Tool nicht in einem einzigen Roundtrip fertig werden. Es braucht etwas, das nur die Person am Host hat: eine Auswahl, eine Bestätigung, Zugangsdaten.
Vor 2026-07-28 holte der Server sich das, indem er zurückrief: Mitten in der Bearbeitung des ursprünglichen Requests öffnete er einen eigenen Request an den Client (eine Elicitation – also eine Rückfrage bei der Person am Host – oder einen Sampling-Aufruf). Die Spec 2026-07-28 schafft diesen Rückkanal (back-channel) ab.
Stattdessen gibt der Server etwas zurück.
Zurückgeben statt zurückrufen
Der Server beantwortet tools/call mit einem InputRequiredResult statt mit einem CallToolResult. Zwei seiner Felder erledigen die Arbeit:
input_requests: was der Server noch braucht, als Dict mit Schlüsseln, die der Server selbst gewählt hat. Jeder Wert ist einElicitRequest, einCreateMessageRequestoder einListRootsRequest.request_state: ein opakes Token. Der Client schickt es beim Retry unverändert zurück. Dein Server ist der Einzige, der es liest.
Der Client erfüllt jeden Request und ruft dann dasselbe Tool noch einmal auf, mit seinen Antworten in input_responses und dem Token in request_state. Der Server hat jetzt, was ihm fehlte, und gibt ein normales CallToolResult zurück.
Das ist das ganze Protokoll. Jede Etappe ist ein gewöhnlicher Request vom Client an den Server. Nie fließt etwas in die andere Richtung.
Die Serverseite
Auf @mcp.tool() baust du das selten von Hand: Deklariere eine Abhängigkeit, die bei der Person am Host zurückfragt (Elicit), das LLM des Clients per Sampling nutzt (Sample) oder seine Roots auflistet (ListRoots), und das SDK gibt das InputRequiredResult für dich zurück; diese Form beschreibt die Seite Abhängigkeiten. Die beiden Formen lassen sich nicht mischen: Ein Aufruf hat genau einen input_responses/request_state-Kanal, deshalb kann ein Tool, das Resolve(...)-Parameter verwendet, nicht zusätzlich ein InputRequiredResult aus seinem Rumpf zurückgeben. Ein deklarierter InputRequiredResult-Rückgabetyp wird bei der Registrierung abgelehnt (InvalidSignature), ein nicht deklarierter lässt den Aufruf zur Laufzeit fehlschlagen. Die manuelle Form ist der Low-Level-Server, dessen Handler on_call_tool beide Ergebnistypen zurückgeben darf:
from mcp.server import Server, ServerRequestContext
from mcp.types import (
CallToolRequestParams,
CallToolResult,
ElicitRequest,
ElicitRequestFormParams,
ElicitResult,
InputRequiredResult,
ListToolsResult,
PaginatedRequestParams,
TextContent,
Tool,
)
ASK_REGION = ElicitRequest(
params=ElicitRequestFormParams(
message="Which region should the database live in?",
requested_schema={
"type": "object",
"properties": {"region": {"type": "string"}},
"required": ["region"],
},
)
)
async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
return ListToolsResult(
tools=[
Tool(
name="provision",
description="Provision a database. Asks which region to put it in.",
input_schema={
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"],
},
)
]
)
async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult | InputRequiredResult:
answer = (params.input_responses or {}).get("region")
if not isinstance(answer, ElicitResult) or answer.content is None:
return InputRequiredResult(input_requests={"region": ASK_REGION}, request_state="provision-v1")
name = (params.arguments or {})["name"]
text = f"Provisioned {name!r} in {answer.content['region']}."
return CallToolResult(content=[TextContent(type="text", text=text)])
server = Server("Provisioner", on_list_tools=list_tools, on_call_tool=call_tool)
on_call_toolist typisiert als-> CallToolResult | InputRequiredResult. Das zweite zurückzugeben ist die gesamte serverseitige API.- Beim ersten Aufruf ist
params.input_responsesNone, also greift die Guard-Bedingung, und der Handler fragt, statt zu antworten. - Beim Retry liegt das
ElicitResult, das der Client geschickt hat, unter demselben Schlüssel ("region"), den der Server ininput_requestsverwendet hat.
Alles andere in dieser Datei (das explizite input_schema, das von Hand gebaute CallToolResult) ist der gewöhnliche Low-Level-Server, behandelt in Der Low-Level-Server. Diese Seite fügt nur den zweiten Rückgabetyp hinzu.
Über Tools hinaus
tools/call ist nichts Besonderes: Unter 2026-07-28 darf ein Server prompts/get und resources/read genauso beantworten. Auf MCPServer gibt eine @mcp.prompt()-Funktion – oder eine @mcp.resource()-Template-Funktion – das InputRequiredResult selbst zurück und liest die Antworten des Retrys aus dem Context:
from mcp.server.mcpserver import Context, MCPServer
from mcp.server.mcpserver.prompts.base import UserMessage
from mcp.types import ElicitRequest, ElicitRequestFormParams, ElicitResult, InputRequiredResult
mcp = MCPServer("Briefing")
ASK_AUDIENCE = ElicitRequest(
params=ElicitRequestFormParams(
message="Who is the briefing for?",
requested_schema={
"type": "object",
"properties": {"audience": {"type": "string"}},
"required": ["audience"],
},
)
)
@mcp.prompt()
async def briefing(ctx: Context) -> list[UserMessage] | InputRequiredResult:
"""Draft a briefing tuned to its audience."""
answer = (ctx.input_responses or {}).get("audience")
if not isinstance(answer, ElicitResult) or answer.content is None:
return InputRequiredResult(input_requests={"audience": ASK_AUDIENCE})
return [UserMessage(f"Write a briefing for {answer.content['audience']}.")]
- Die erste Runde gibt das
InputRequiredResultzurück. Beim Retry hältctx.input_responsesdie Antworten unter denselben Schlüsseln bereit, und die Funktion gibt ihr gewöhnliches Ergebnis zurück – hier Prompt-Nachrichten, bei einer Template-Ressource Ressourceninhalt. - Ein
request_state, den du setzt, wird versiegelt, bevor er über die Leitung geht, und beim Echo verifiziert, wie alles andere auf dem Server;requestStateschützen weiter unten beschreibt, was dir das Siegel bringt und wann du Schlüssel konfigurieren musst. - Eine
@mcp.tool()-Funktion kann das Ergebnis genauso direkt zurückgeben, wenn die Abhängigkeitsform nicht passt. - Statische
@mcp.resource()-Funktionen nehmen nicht teil: Sie bekommen keinenContextund könnten den Retry deshalb nie lesen. Nur Template-Ressourcen können fragen. - Die Regeln zur Protokollgeneration weiter unten gelten unverändert: Ein
InputRequiredResultauf einer Session vor 2026 zurückzugeben, ergibt denselben-32603, den die Warnung beschreibt.
Die Clientseite
Client führt die Schleife für dich aus.
Registriere die Callbacks, nach denen der Server fragen könnte (elicitation_callback, sampling_callback, list_roots_callback), und rufe das Tool auf. Kommt ein InputRequiredResult an, verteilt Client jeden Eintrag in input_requests an den passenden Callback, wiederholt den Aufruf mit den Antworten und dem zurückgeschickten request_state und macht weiter, bis ein CallToolResult zurückkommt:
from mcp import Client
from mcp.client import ClientRequestContext
from mcp.types import ElicitRequestParams, ElicitResult
async def handle_elicitation(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult:
return ElicitResult(action="accept", content={"region": "eu-west-1"})
async def main() -> None:
async with Client("http://127.0.0.1:8000/mcp", elicitation_callback=handle_elicitation) as client:
result = await client.call_tool("provision", {"name": "orders"})
print(result.content)
- Dieser
elicitation_callbackist derselbe, den daselicitation/createeines Servers vor 2026 über den Rückkanal getroffen hätte. Dasselbe gilt fürsampling_callbackbeisampling/createMessageund fürlist_roots_callbackbeiroots/list: Unter 2026-07-28 sind die eigenständigen Server->Client-RPCs verschwunden, aber die identischen PayloadsElicitRequest/CreateMessageRequest/ListRootsRequestreisen ininput_requestsmit und landen bei denselben drei Callbacks. Ein Satz Callbacks bedient beide Generationen. call_toolgibt ein schlichtesCallToolResultzurück. Die Zwischenrunden sind für den aufrufenden Code unsichtbar.get_promptundread_resourcetreiben dieselbe Schleife.
Check
Lässt du den Callback weg, scheitert die Schleife in der ersten Runde: Der Ersatz-Callback des SDK
beantwortet jede Elicitation mit einem Fehler, und call_tool löst MCPError mit der Meldung
„Elicitation not supported“ aus.
Die Schleife ist begrenzt. Client(..., input_required_max_rounds=10) ist die Standardobergrenze; ein Server, der darüber hinaus weiter InputRequiredResult zurückgibt, lässt call_tool eine Exception auslösen. Trägt eine Runde nur request_state und keine input_requests, schläft Client kurz (50 ms, verdoppelt bis zu einer Obergrenze von 250 ms), bevor er es erneut versucht. So wird ein Server, der nur „noch nicht fertig“ sagt, nicht in einer Dauerschleife abgefragt.
Die Schleife selbst steuern
Die automatische Schleife genügt für einen Client in einem einzigen Prozess. Übernimm die Schleife stattdessen selbst, wenn:
- dein Client verteilt ist: Der Prozess, der der Person die Frage anzeigt, ist nicht der Prozess, der
call_toolaufgerufen hat, also setzt ein anderer Worker den Retry ab.request_stateist das persistierbare Token, das du über diese Grenze trägst – durch deinen eigenen Speicher –, undinput_responsesist das, was die andere Seite damit zurückschickt. - du jede Runde inspizieren willst: jeden
input_requests-Eintrag loggen oder auditieren, bestimmte Request-Arten ablehnen oder zwischen den Etappen ein eigenes Backoff anwenden. - du eine Grenze nach Uhrzeit statt nach Rundenzahl willst: Umschließe deine eigene Schleife mit
anyio.fail_after(...), statt dich aufinput_required_max_roundszu verlassen.
Geh auf die darunterliegende Session hinunter, wo allow_input_required=True dir die Union direkt aushändigt:
from mcp import Client
from mcp.types import CallToolResult, ElicitRequest, ElicitResult, InputRequest, InputRequiredResult, InputResponse
def fulfil(request: InputRequest) -> InputResponse:
if not isinstance(request, ElicitRequest):
raise NotImplementedError(f"this client cannot answer a {request.method!r} request")
return ElicitResult(action="accept", content={"region": "eu-west-1"})
async def provision(client: Client, name: str) -> CallToolResult:
result = await client.session.call_tool("provision", {"name": name}, allow_input_required=True)
while isinstance(result, InputRequiredResult):
responses = {key: fulfil(request) for key, request in (result.input_requests or {}).items()}
result = await client.session.call_tool(
"provision",
{"name": name},
input_responses=responses,
request_state=result.request_state,
allow_input_required=True,
)
return result
client.session.call_tool(..., allow_input_required=True)erweitert den Rückgabetyp aufCallToolResult | InputRequiredResult. Dasisinstanceengt ihn wieder ein.request_stateliegt jetzt in deiner Hand. Schreib ihn zwischen den Etappen weg, und das Gespräch kann aus einem frischen Prozess fortgesetzt werden.- Für jeden Eintrag in
input_requestslegst du eineInputResponseunter demselben Schlüssel ininput_responsesab.fulfilist die Stelle für deine UI; diese hier kodiert die Antwort fest. - Derselbe Tool-Name, dieselben
arguments, in jeder Etappe. Der Retry ist der ursprüngliche Aufruf, noch einmal ausgeführt, keine neue Methode.
requestState schützen
Alles oben behandelt request_state als Echo, und auf der Leitung ist er auch nichts anderes. Aber der Client hält ihn zwischen den Etappen (ihn über Prozesse hinweg wegzuschreiben ist genau das, was der vorige Abschnitt abgesegnet hat), also ist das, was zurückkommt, vom Client gelieferte Eingabe: Sie kann verändert, abgelaufen oder aus einem ganz anderen Aufruf entnommen sein. Die Spec verlangt von Servern, die Integrität dieses Zustands zu schützen und die Runde abzulehnen, wenn die Verifikation fehlschlägt – immer dann, wenn der Zustand Autorisierung, Ressourcenzugriff oder Geschäftslogik beeinflussen kann.
MCPServer schützt ihn standardmäßig. Jeder Server versiegelt ausgehenden requestState und verifiziert jedes Echo – Resolver-Zustand und von Hand gebauten Zustand gleichermaßen – unter einem Schlüssel, der beim Prozessstart erzeugt wird. Du konfigurierst nichts, schreibst Klartext und liest Klartext; über die Leitung geht immer nur ein opakes, verschlüsseltes Token.
Der Standardschlüssel lebt und stirbt mit dem Prozess – das ist das Eine, was du wissen musst, bevor du über einen einzelnen Prozess hinaus bereitstellst:
from mcp.server.mcpserver import MCPServer, RequestStateSecurity
# Multi-instance or restart-surviving: one or more shared secret keys (>= 32 bytes each).
mcp = MCPServer("fleet", request_state_security=RequestStateSecurity(keys=[key]))
- Der Standard (keine Konfiguration) passt für einen einzelnen Prozess: stdio oder genau ein HTTP-Worker. Ein Retry, der bei einem anderen Worker, einer anderen Instanz hinter einem Load Balancer oder demselben Server nach einem Neustart landet, ist unter einem Schlüssel versiegelt, den dieser Prozess nicht hat – der Client bekommt die unten beschriebene feste Ablehnung und muss den Ablauf von vorn beginnen.
keys=[...]ist erforderlich, sobald ein Retry eine andere Instanz erreichen kann (uvicornmit mehreren Workern, HTTP hinter Lastverteilung) oder Neustarts überleben muss: Jede Instanz verifiziert, was irgendeine Schwesterinstanz ausgestellt hat. Dieselbe Maschinerie, dein Geheimnis statt eines erzeugten.- Für eigene Kryptografie, etwa ein KMS oder einen vorhandenen Token-Dienst, übergib
RequestStateSecurity(codec=...)stattkeys; Eigene Kryptografie mitbringen weiter unten beschreibt den Vertrag.
Was das Siegel trägt
Ob Standard oder konfiguriert: requestState auf der Leitung ist ein verschlüsseltes, authentifiziertes Token. Dein Code sieht es nie: Handler und Resolver schreiben Klartext und lesen Klartext (ctx.request_state); das SDK versiegelt auf dem Weg hinaus und verifiziert auf dem Weg hinein. Über die Integrität hinaus ist jedes Token gebunden an:
- Ein Zeitfenster. Jede Runde versiegelt neu mit frischem Ablaufzeitpunkt, deshalb begrenzt
RequestStateSecurity(ttl=...)(Standardwert 600 Sekunden) die Bedenkzeit pro Runde, nicht den ganzen Ablauf. - Den authentifizierten Principal. Trägt der Request ein OAuth-Access-Token, das das SDK validiert hat, wird der Zustand an Client, Issuer und Subject des Tokens gebunden: Zustand, der für eine Person ausgestellt wurde, scheitert unter einer anderen, selbst wenn beide denselben OAuth-Client teilen. Ein Verifier, der kein Subject liefert, schwächt die Bindung auf die Client-Identität allein ab, die bei URL-basierten Client-IDs alle teilen, die diese Client-Software verwenden. Wird die Authentifizierung außerhalb des SDK terminiert (ein vorgeschalteter Proxy) oder ist der Transport nicht authentifiziert, gibt es keinen Principal zum Binden, und diese Prüfung bleibt wirkungslos – es sei denn,
RequestStateSecurity(bind_principal=...)liefert einen aus deinem eigenen Identitätssignal. Welche Bestandteile dein Token-Verifier auch liefert, er muss sie konsistent liefern: Ein Verifier, der das Subject bei manchen Requests einschließt und bei anderen weglässt, ändert den Principal mitten im Ablauf, und laufende Runden werden abgelehnt. - Den auslösenden Request. Die Methode, den Tool- oder Prompt-Namen (oder den Ressourcen-URI) und einen Digest der Argumente. Ein Token, das gegen ein anderes Tool, andere Argumente oder eine andere Methode wieder eingespielt wird, scheitert.
- Die genaue gestellte Frage. Jede Resolver-Antwort ist an die gerenderte Frage geheftet, die dem Client gezeigt wurde, sowohl in der Runde, in der sie zuerst eintrifft, als auch wenn eine aufgezeichnete Antwort später wiederverwendet wird. Stellst du mit umformulierter Nachricht oder geändertem Schema neu bereit, fragt der Server erneut, statt eine veraltete Antwort zu verbrauchen. Dieselbe Bindung wirkt auch andersherum: Leite Nachrichten aus den Argumenten des Tools ab, nicht aus Daten pro Aufruf. Eine Nachricht, die aus einem Zeitstempel oder einem Live-Kurs gebaut ist, rendert in jeder Runde anders, sodass jede aufgezeichnete Antwort veraltet aussieht und der Server erneut fragt, bis das Rundenlimit des Clients den Aufruf beendet.
All das ist Aufgabe des SDK, nicht deine – und nicht die des Codecs, falls du deinen eigenen mitbringst.
Schlüssel rotieren
keys[0] versiegelt neuen Zustand; jeder Schlüssel in der Liste verifiziert. Eine Rotation ohne Ausfallzeit besteht aus drei Phasen, jede vollständig ausgerollt, bevor die nächste beginnt:
RequestStateSecurity(keys=[OLD, NEW]) # 1: every instance learns to verify NEW; OLD still mints
RequestStateSecurity(keys=[NEW, OLD]) # 2: NEW mints; in-flight OLD state keeps verifying
RequestStateSecurity(keys=[NEW]) # 3: one ttl after phase 2 is fully out, retire OLD
Befördere niemals zuerst den ausstellenden Schlüssel: Unter einem Schlüssel auszustellen, den manche Instanz noch nicht verifizieren kann, lässt laufende Runden mitten im Rollout fallen.
Schlüssel gelten für genau einen Dienst. Der versiegelte Umschlag trägt außerdem den Namen des Servers als Audience-Claim, sodass ein Token, das ein anderer Dienst ausgestellt hat, der zufällig ein Geheimnis teilt, trotzdem abgelehnt wird. Der Claim ist nur so unterscheidungskräftig wie der Name, deshalb muss ein Server mit expliziter Policy einen echten Namen haben oder RequestStateSecurity(audience=...) setzen – ein unbenannter löst bei der Konstruktion eine Exception aus. audience= dient auch bewussten Multi-Service-Topologien, in denen ein Dienst Zustand akzeptieren muss, den ein anderer ausgestellt hat. (Der konfigurationsfreie Standard ist ausgenommen: Sein Schlüssel verlässt den Prozess nie, also hat der Audience-Claim nichts hinzuzufügen.)
Eigene Kryptografie mitbringen
RequestStateSecurity(codec=...) nimmt alles mit seal(bytes) -> str und unseal(str) -> bytes, das für jedes Token, das es nicht selbst ausgestellt hat, InvalidRequestState auslöst. Die klassische Form ist Envelope Encryption gegen ein KMS, bei der du beim Start einmal einen Datenschlüssel entpackst und die Kryptografie pro Token lokal hältst:
import os
from cryptography.exceptions import InvalidTag
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from mcp.server import MCPServer
from mcp.server.mcpserver import InvalidRequestState, RequestStateSecurity
PREFIX = "kms1." # format version; fed to GCM as associated data, so it is bound under the tag
def unwrap_data_key() -> bytes:
"""One KMS call at process start, kms.decrypt(CiphertextBlob=...); every token after that is local crypto."""
return os.urandom(32) # stand-in for the unwrapped 32-byte data key
class EnvelopeCodec:
def __init__(self, data_key: bytes) -> None:
self._aesgcm = AESGCM(data_key)
def seal(self, payload: bytes) -> str:
nonce = os.urandom(12)
return PREFIX + (nonce + self._aesgcm.encrypt(nonce, payload, PREFIX.encode())).hex()
def unseal(self, token: str) -> bytes:
if not token.startswith(PREFIX):
raise InvalidRequestState("unknown token format")
body = token[len(PREFIX) :]
try:
raw = bytes.fromhex(body)
if raw.hex() != body: # only the exact string seal() produced verifies
raise ValueError("non-canonical hex")
return self._aesgcm.decrypt(raw[:12], raw[12:], PREFIX.encode())
except (ValueError, InvalidTag) as exc:
raise InvalidRequestState("token failed verification") from exc
mcp = MCPServer("Deployer", request_state_security=RequestStateSecurity(codec=EnvelopeCodec(unwrap_data_key())))
TTL, Principal-Bindung und Request-Bindung sind nicht Sache des Codecs: Das SDK stempelt sie vor seal in die Payload und verifiziert sie nach unseal erneut, für jeden Codec. Die einzigen Pflichten eines Codecs sind Integrität (manipuliert heißt: Exception auslösen) und idealerweise Vertraulichkeit.
Wenn die Verifikation fehlschlägt
Jeder eingehende Fehlschlag – ob manipuliert, abgelaufen, gegen einen anderen Request oder Principal wieder eingespielt oder unter einem Schlüssel versiegelt, den dieser Server nicht kennt – bekommt dieselbe Antwort:
{"code": -32602, "message": "Invalid or expired requestState"}
Eine feste Meldung für jede Ursache, damit die Leitung nie verrät, welche Prüfung fehlschlug; der wahre Grund geht ins Server-Log. Jeder eingehende requestState auf tools/call, prompts/get und resources/read wird geprüft, auch einer, der für einen Handler eintrifft, der nie Zustand ausstellt. Die in der Praxis häufigste Ablehnung ist kein Angriff – es ist der prozesslokale Standardschlüssel, der auf einen Retry von vor einem Neustart oder von einer anderen Instanz trifft; der Client startet den Ablauf neu, und keys=[...] ist die Lösung, wenn das ins Gewicht fällt.
Von Hand gebauter Zustand
Ein request_state, den du selbst setzt (indem du InputRequiredResult aus einer Tool-, Prompt- oder Ressourcen-Template-Funktion zurückgibst), wird von derselben Maschinerie versiegelt und verifiziert wie Resolver-Zustand, ganz ohne Codeänderungen: Klartext schreiben, Klartext lesen, und jede Bindung oben gilt.
Das Eine, was das SDK dir nicht festheften kann, selbst wenn konfiguriert, ist die Identität der Frage: Es weiß nicht, zu welcher deiner Fragen eine Antwort in deinem Zustand gehört. Speicherst du Antworten nach Fragen geschlüsselt, nimm deine eigene Fragekennung in den Zustand auf und prüfe sie beim Retry.
Der Low-Level-Server ist die Stufe ohne Extras: Anders als bei MCPServer wird nichts versiegelt, bis du die Grenze selbst anhängst, und bis dahin geht dein request_state genau so über die Leitung, wie du ihn geschrieben hast. Das einzeilige Opt-in zeigt Der Low-Level-Server.
Ein Ergebnis für 2026-07-28
InputRequiredResult gibt es nur bei Protokollversion 2026-07-28. Der In-Memory-Client(server) handelt sie für dich aus; über die Leitung entdeckt mode="auto" sie. Nach dem Verbinden sagt dir client.protocol_version, was du bekommen hast.
Warning
Eine Session vor 2026 hat keinen Platz für ein InputRequiredResult. Gibst du eines aus deinem Handler auf einer
mode="legacy"-Verbindung zurück, kann der Runner es nicht in die ausgehandelte Version serialisieren; der
Client bekommt einen -32603-Fehler „Handler returned an invalid result“ zurück. Ein Server, der
beide Generationen bedient, muss ctx.protocol_version prüfen, bevor er danach greift.
Info
Elicitation im URL-Modus nutzt auf einer 2026er-Verbindung genau diesen Mechanismus. Der Eintrag in
input_requests ist ein ElicitRequest, dessen Params ElicitRequestURLParams sind; die Person
schließt den Out-of-band-Ablauf ab, und dein Client wiederholt den Aufruf. Dieselbe Schleife, keine neue API. Die
Hälfte für den High-Level-Server steht in Elicitation.
Zusammenfassung
- Unter 2026-07-28 gibt ein Server, der mitten im Aufruf Eingaben braucht, ein
InputRequiredResultzurück. Er öffnet nie einen Request an den Client. input_requestsist, was er braucht.request_stateist ein opakes Wiederaufnahme-Token, das nur der Server liest.Clientführt die Retry-Schleife für dich aus: Registriereelicitation_callback/sampling_callback/list_roots_callback, undcall_toolgibt ein schlichtesCallToolResultzurück.input_required_max_rounds(Standardwert 10) begrenzt sie.- Um Runden zu inspizieren oder zu persistieren, verwende
client.session.call_tool(..., allow_input_required=True)und übernimm die Schleifewhile isinstance(result, InputRequiredResult)selbst. - Auf
@mcp.tool()erzeugt eine Abhängigkeit, die bei der Person am Host zurückfragt, dieses Ergebnis für dich (Abhängigkeiten); der Low-Level-Serverist die manuelle Form. - Prompts und Ressourcen nehmen ebenfalls teil: Eine
@mcp.prompt()- oder Template-@mcp.resource()-Funktion gibt dasInputRequiredResultselbst zurück und liest beim Retryctx.input_responses. requestStatekommt als vom Client gelieferte Eingabe zurück, deshalb versiegeltMCPServerihn standardmäßig – Resolver-Zustand und von Hand gebauten Zustand gleichermaßen – unter einem prozesslokalen Schlüssel; Deployments mit mehreren Instanzen übergebenRequestStateSecurity(keys=[...])(oder einen eigenen Codec), damit jede Instanz verifizieren kann, was eine Schwesterinstanz ausgestellt hat. Das Siegel bindet jedes Token an ein Zeitfenster, den auslösenden Request und den authentifizierten Principal, wenn der Request eine vom SDK validierte Authentifizierung trägt oderbind_principal=dein eigenes Identitätssignal liefert (requestStateschützen).
Das ist der Mechanismus, der serverinitiiertes Sampling und den Rest des Push-artigen Rückkanals ersetzt; siehe Veraltete Features.