Fonctions de rappel du client
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.
Presque toutes les requêtes dans MCP vont dans un seul sens : du client vers le serveur.
Un serveur peut aussi demander des choses au client : poser une question à l’utilisateur, échantillonner le modèle de l’utilisateur, lister les dossiers de son espace de travail. Vous répondez à ces requêtes en passant des fonctions de rappel (callbacks) à Client(...).
Un serveur qui demande
Voici un serveur dont l’outil ne peut pas terminer tout seul :
from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
mcp = MCPServer("Library")
class CardHolder(BaseModel):
name: str
@mcp.tool()
async def issue_card(ctx: Context) -> str:
"""Issue a new library card."""
answer = await ctx.elicit("What name should go on the card?", schema=CardHolder)
if answer.action == "accept":
return f"Card issued to {answer.data.name}."
return "No card issued."
ctx.elicit(...)envoie une requêteelicitation/createau client et attend.- L’outil ne renvoie rien tant que quelqu’un (une personne devant un formulaire, ou votre code) n’a pas fourni un
name.
C’est la moitié serveur, et la page Élicitation la couvre en détail. Cette page-ci se tient à l’autre bout de la liaison.
La fonction de rappel d’élicitation
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={"name": "Ada Lovelace"})
async def main() -> None:
async with Client(
"http://127.0.0.1:8000/mcp",
mode="legacy",
elicitation_callback=handle_elicitation,
) as client:
result = await client.call_tool("issue_card")
print(result.content)
- Une fonction de rappel d’élicitation (elicitation) a pour signature
async (context, params) -> ElicitResult. params.messageest la question.params.requested_schemaest le JSON Schema de la réponse que le serveur attend. Un vrai client en tire un formulaire ; celui-ci le remplit automatiquement.- Vous renvoyez
ElicitResult(action="accept", content={...}), ouaction="decline", ouaction="cancel". La seule autre option estErrorData(...), qui refuse la requête et fait échouer l’appel entier. contextest unClientRequestContext: lasessionactive, lerequest_iddu serveur et les éventuellesmetaqu’il a jointes.
Tip
params est une union des deux modes d’élicitation. Ici params.mode vaut "form" ; une requête "url"
porte params.url au lieu d’un schéma. Une seule fonction de rappel gère les deux ; branchez sur params.mode.
Élicitation montre le motif complet.
Essayer
Appelez issue_card et observez les deux extrémités.
Votre fonction de rappel reçoit la question du serveur, déjà analysée :
params.mode # 'form'
params.message # 'What name should go on the card?'
params.requested_schema # {'properties': {'name': {'title': 'Name', 'type': 'string'}},
# 'required': ['name'], 'title': 'CardHolder', 'type': 'object'}
Elle répond, ctx.elicit(...) reprend à l’intérieur de l’outil, et l’outil termine :
result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.')]
Un tools/call de votre part, un elicitation/create en retour du serveur, auquel votre fonction répond, le tout à l’intérieur d’un seul appel d’outil.
Info
mode="legacy" dans l’appel Client(...) fait un vrai travail. Par défaut, Client(...) négocie le chemin
moderne du protocole, et ce chemin n’a pas de canal de retour (back-channel) pour les requêtes du serveur vers le client : ctx.elicit
échoue avant même que votre fonction de rappel ne s’exécute. Ce n’est pas le transport qui en décide ; c’est le
protocole négocié, en mémoire comme via une URL. Fixez mode="legacy" dès que votre client doit
répondre à l’une d’elles ; tous les tests derrière cette page le font. Tous les détails sont dans Versions du protocole.
Sur une session 2026-07-28, la fonction de rappel n’est pas morte, elle est alimentée autrement : quand un outil renvoie un
InputRequiredResult portant une ElicitRequest, Client transmet cette entrée à la même
elicitation_callback et relance l’appel pour vous. Ce flux est décrit dans Requêtes à plusieurs allers-retours (multi-round-trip).
Une fonction de rappel est une capacité
Vous n’avez jamais dit au serveur que votre client sait répondre aux requêtes d’élicitation. Le SDK l’a fait.
Quand un client se connecte, il déclare ses capabilities, l’image miroir de celles du serveur. Vous n’écrivez pas cet objet. Enregistrer une fonction de rappel vaut déclaration.
| vous passez | le client déclare |
|---|---|
elicitation_callback= |
"elicitation": {"form": {}, "url": {}} |
sampling_callback= |
"sampling": {} |
list_roots_callback= |
"roots": {"listChanged": true} |
| aucune d’elles | {} |
Les sous-capacités d’échantillonnage (sampling) sont le seul raffinement : passez sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability()) en plus de sampling_callback lorsque votre échantillonneur gère les paramètres tools / tool_choice. Les serveurs doivent voir sampling.tools déclaré avant de pouvoir les envoyer.
logging_callback et message_handler ne figurent pas dans le tableau. Ils traitent des notifications, et les notifications n’exigent aucune capacité.
Le serveur relit la déclaration avec ctx.session.check_client_capability(...). Ajoutez un outil qui le fait :
from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
from mcp.types import ClientCapabilities, ElicitationCapability, RootsCapability, SamplingCapability
mcp = MCPServer("Library")
class CardHolder(BaseModel):
name: str
@mcp.tool()
async def issue_card(ctx: Context) -> str:
"""Issue a new library card."""
answer = await ctx.elicit("What name should go on the card?", schema=CardHolder)
if answer.action == "accept":
return f"Card issued to {answer.data.name}."
return "No card issued."
@mcp.tool()
def client_features(ctx: Context) -> list[str]:
"""Which optional features the connected client declared."""
declared = {
"elicitation": ClientCapabilities(elicitation=ElicitationCapability()),
"sampling": ClientCapabilities(sampling=SamplingCapability()),
"roots": ClientCapabilities(roots=RootsCapability()),
}
return [name for name, capability in declared.items() if ctx.session.check_client_capability(capability)]
Connectez-vous avec seulement elicitation_callback et appelez-le :
result.structured_content # {'result': ['elicitation']}
Passez les trois fonctions de rappel et vous obtenez ['elicitation', 'sampling', 'roots']. N’en passez aucune et vous obtenez [].
Check
Faites maintenant ce qu’il ne faut pas : connectez-vous sans elicitation_callback et appelez issue_card quand même.
La requête elicitation/create du serveur atteint toujours votre client, et le SDK y répond à votre
place, par une erreur, puisque vous n’avez jamais dit pouvoir la traiter. Cette erreur coule l’appel entier.
call_tool ne renvoie pas un résultat is_error ; il lève une exception :
MCPError: Elicitation not supported
C’est une erreur de protocole (-32600, requête invalide), pas une erreur d’outil : le modèle n’a rien
à lire ni à retenter. C’est pourquoi client_features vaut la peine : un serveur bien élevé
vérifie avant de demander.
La paire obsolète
sampling_callback répond à sampling/createMessage : le serveur demande à votre modèle de compléter quelque chose. list_roots_callback répond à roots/list : le serveur demande dans quels répertoires il peut travailler.
Les deux fonctionnent. Les deux suivent la règle ci-dessus. Et les deux servent des RPC que la spécification 2026-07-28 supprime : un serveur moderne ne rappelle pas votre client en pleine requête, il vous rend la requête dans le résultat de l’outil (Requêtes à plusieurs allers-retours). Les fonctions de rappel elles-mêmes ne sont pas mortes. Quand un InputRequiredResult porte une CreateMessageRequest ou une ListRootsRequest, la boucle automatique de Client la transmet à la même sampling_callback ou list_roots_callback que vous avez enregistrée ici. La liste complète est dans Fonctionnalités obsolètes.
Vous avez encore besoin de ces fonctions de rappel pour parler aux serveurs qui n’ont pas migré. Les signatures :
from pydantic import FileUrl
from mcp.client import ClientRequestContext
from mcp.types import CreateMessageRequestParams, CreateMessageResult, ListRootsResult, Root, TextContent
async def handle_sampling(
context: ClientRequestContext,
params: CreateMessageRequestParams,
) -> CreateMessageResult:
return CreateMessageResult(
role="assistant",
content=TextContent(type="text", text="The answer is 42."),
model="my-llm",
)
async def handle_list_roots(context: ClientRequestContext) -> ListRootsResult:
return ListRootsResult(roots=[Root(uri=FileUrl("file:///home/ada/notebooks"), name="notebooks")])
- Une fonction de rappel d’échantillonnage reçoit le
CreateMessageRequestParamscomplet (messages,model_preferences,max_tokens) et renvoie unCreateMessageResult. C’est vous qui exécutez le modèle, comme bon vous semble ; le SDK ne fait que transporter la requête. - Une fonction de rappel de racines (roots) ne prend aucun paramètre et renvoie un
ListRootsResult. - L’une comme l’autre peut renvoyer
ErrorData(...)à la place, pour refuser.
Passez-les à Client(...) exactement comme elicitation_callback.
Les fonctions de rappel de notification
Deux de plus. Aucune ne déclare quoi que ce soit.
logging_callback reçoit les notifications/message qu’un serveur envoie, sous forme de LoggingMessageNotificationParams (level, logger, data). La journalisation par le protocole est elle-même rendue obsolète par la spécification 2026-07-28 (Journalisation explique quoi faire à la place), donc cette fonction de rappel existe pour les serveurs qui l’émettent encore. Sur une connexion de génération 2026, la fonction de rappel seule ne vous apporte rien, car les serveurs 2026 n’envoient des messages de journal qu’aux requêtes qui en font la demande : passez log_level="info" (ou un autre niveau) à Client(...) pour apposer cette demande sur chaque requête et recevoir ce niveau et les niveaux supérieurs. Les serveurs antérieurs à 2026 l’ignorent et conservent leur comportement logging/setLevel.
message_handler est le fourre-tout : chaque notification serveur que la session remonte lui parvient (en plus de sa fonction de rappel spécifique), et sur un transport adossé à un flux, chaque Exception de niveau transport aussi. Deux n’y parviennent jamais : notifications/cancelled est appliquée par le SDK plutôt que remontée, et l’accusé de réception d’abonnement d’un flux listen() actif est consommé par ce flux. Annotez le paramètre avec IncomingMessage (ServerNotification | Exception, exporté depuis mcp.client). Le seul motif à connaître est if isinstance(message, Exception): raise message, pour qu’une connexion rompue échoue bruyamment au lieu de disparaître en silence.
Récapitulatif
- Un serveur peut envoyer des requêtes au client. Vous y répondez avec des fonctions de rappel passées à
Client(...). - La fonction de rappel d’élicitation est celle d’actualité :
async (context, params) -> ElicitResult, une seule fonction pour les modes formulaire et URL. - Enregistrer une fonction de rappel, c’est déclarer la capacité. Sans elle, le SDK refuse la requête du serveur à votre place et l’appel entier échoue avec
MCPError. - Un serveur le sait avant de demander grâce à
ctx.session.check_client_capability(...). sampling_callbacketlist_roots_callbackfonctionnent de la même manière mais servent des fonctionnalités obsolètes ; les serveurs modernes utilisent à la place les requêtes à plusieurs allers-retours.logging_callbacketmessage_handlerreçoivent des notifications. Ils ne déclarent rien.
Le premier argument de Client(...) est un objet transport. Transports client couvre tous les types.