docs: publish translated docs in twelve languages and the tool that maintains them (#3280)

This commit is contained in:
Max 2026-08-14 17:07:34 +01:00 • committed by GitHub
commit 52ad0a8876
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 89737 additions and 54 deletions

View file

@ -0,0 +1,134 @@
---
translation:
sections: [b50152f05c81e786, b302059b22fb7cb4, 85682a1bf561243a, 53fc48838eb6837a, b24190e0842786ec, 85f93e150fc9b240]
tool: 1
---
# L’objet Context {#the-context}
Les arguments d’un outil viennent du modèle. Tout le reste (la requête que vous servez, le serveur dans lequel vous vivez, un moyen de répondre au client) vient d’un seul objet : le **`Context`**.
Vous ne le construisez pas, vous ne le configurez pas. Vous le demandez.
## Le demander {#ask-for-it}
Ajoutez un paramètre annoté avec `Context` à n’importe quel outil :
```python title="server.py" hl_lines="2 8"
--8<-- "docs_src/context/tutorial001.py"
```
* Le SDK construit un `Context` neuf pour chaque requête et vous le passe.
* Le **nom du paramètre n’a aucune importance**. `ctx`, `context`, `c` : le SDK le trouve grâce à son annotation.
* Les ressources et les prompts peuvent en déclarer un aussi, de la même façon.
* `ctx.request_id` est l’identifiant de la requête que votre fonction est en train de servir.
!!! info
Si vous avez utilisé FastAPI, vous connaissez le procédé : vous déclarez un paramètre avec le type propre au framework
(`Request` là-bas, `Context` ici) et le framework le fournit. Rien à enregistrer, rien à
configurer : l’annotation de type est tout le mécanisme.
### Invisible pour le modèle {#invisible-to-the-model}
C’est le point à bien intégrer. Voici le schéma d’entrée que `tools/list` renvoie pour `search_books` :
```json
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"}
},
"required": ["query"],
"title": "search_booksArguments"
}
```
Une seule propriété. `ctx` n’est pas un argument : il n’apparaît jamais dans le schéma, le modèle n’en entend jamais parler et aucun client ne peut le remplir. C’est un contrat entre vous et le SDK, invisible sur la liaison.
### Essayer {#try-it}
Lancez le serveur avec le MCP Inspector :
```console
uv run mcp dev server.py
```
Le formulaire de `search_books` n’a qu’un seul champ, `query`. Appelez-le avec `dune` :
```text
[request 3] Found 3 books matching 'dune'.
```
Le numéro est celui de la requête en question, quelle qu’elle soit. Appelez de nouveau l’outil et il change : chaque requête reçoit son propre `Context`.
## Ce qu’il vous apporte {#what-it-gives-you}
L’objet injecté est petit. En plus de `request_id` :
* `await ctx.read_resource(uri)` : lire l’une des **propres** ressources du serveur depuis un outil. C’est la section suivante.
* `await ctx.report_progress(progress, total, message)` : remonter la progression à l’appelant pendant un appel long. Tous les détails sont dans **[Progression](progress.md)**.
* `await ctx.elicit(message, schema)` et `await ctx.elicit_url(...)` : mettre l’outil en pause et poser une question à l’utilisateur. C’est **[l’élicitation (elicitation)](elicitation.md)**.
* `ctx.session` : le côté serveur de la conversation avec ce client. Les notifications que vous envoyez au client passent par là ; la dernière section s’en sert.
* `ctx.headers` : les en-têtes de requête acheminés par le transport, ou `None` en stdio. Lisez un en-tête personnalisé avec `(ctx.headers or {}).get("x-...")`. Les en-têtes sont des données fournies par le client — très bien pour une langue ou un feature flag, jamais pour une identité.
* `ctx.request_context` : l’enregistrement brut propre à la requête. Le champ que vous irez chercher est `lifespan_context`, l’objet que votre code de démarrage a produit avec yield (voir **[Cycle de vie (lifespan)](lifespan.md)**).
La journalisation est volontairement absente de cette liste. Un serveur journalise avec le module `logging` de Python, comme n’importe quel autre programme Python. **[Journalisation](logging.md)** est la courte page qui explique pourquoi.
!!! tip
L’injection n’a lieu que pour la fonction que vous avez enregistrée. Une fonction auxiliaire appelée par votre outil ne reçoit pas
son propre `Context` ; passez-lui `ctx` comme un argument ordinaire. Il n’existe aucun
« contexte courant » ambiant à récupérer ailleurs.
## Lire vos propres ressources {#read-your-own-resources}
Les ressources d’un serveur ne sont pas réservées aux clients. Un outil peut les lire aussi :
```python title="server.py" hl_lines="16"
--8<-- "docs_src/context/tutorial002.py"
```
`ctx.read_resource` résout l’URI via le même registre que celui qui sert `resources/read`, si bien qu’un outil obtient ce qu’un client obtiendrait : un itérable de `ReadResourceContents`, un par bloc de contenu. Pour cet URI, il y en a un :
```python
contents.content # 'fiction, non-fiction, poetry'
contents.mime_type # 'text/plain'
```
* `content` est exactement ce que `genres()` a renvoyé. Une seule source de vérité : le client parcourt la ressource, vos outils la consomment, personne ne copie la chaîne.
* Le seul paramètre de `describe_catalog` est le `Context`, donc son schéma d’entrée n’a **aucune propriété**. Le modèle l’appelle avec `{}`.
## Signaler au client que la liste a changé {#tell-the-client-the-list-changed}
Ce qu’un serveur propose n’est pas figé au moment de l’import. Enregistrez un outil à l’exécution, puis prévenez le client :
```python title="server.py" hl_lines="15-16"
--8<-- "docs_src/context/tutorial003.py"
```
* `mcp.add_tool(recommend_book)` enregistre une simple fonction comme outil : nom, description et schéma dérivés exactement comme `@mcp.tool()` l’aurait fait.
* `await ctx.session.send_tool_list_changed()` envoie `notifications/tools/list_changed`. Un client qui la reçoit appelle de nouveau `tools/list` et voit `recommend_book`.
Les méthodes sœurs sont `send_resource_list_changed()`, `send_prompt_list_changed()` et `send_resource_updated(uri)` pour un changement sur une ressource précise.
Sur une connexion 2026-07-28, les clients ne reçoivent les notifications de changement que sur un flux `subscriptions/listen` qu’ils ont ouvert ; les méthodes `send_*` ci-dessus n’atteignent donc pas ces flux. Les méthodes de publication du `Context` diffusent vers tous les flux abonnés d’un coup : `await ctx.notify_tools_changed()`, `await ctx.notify_prompts_changed()`, `await ctx.notify_resources_changed()` et `await ctx.notify_resource_updated(uri)`. Tous les détails, y compris la montée en charge sur plusieurs réplicas, sont dans **[Abonnements](subscriptions.md)**.
!!! check
Avant que quelqu’un n’exécute `enable_recommendations`, l’outil que vous promettez n’existe pas. Appelez-le
quand même et le résultat est une erreur que le modèle peut lire :
```text
Unknown tool: recommend_book
```
Exécutez `enable_recommendations`, et le même appel réussit. La liste d’outils est réellement
dynamique : `tools/list` reflète ce qui est enregistré *à l’instant même*.
## Récapitulatif {#recap}
* Annotez un paramètre avec `Context` (dans un outil, une ressource ou un prompt) et le SDK l’injecte. Le nom vous appartient.
* Il est invisible pour le modèle : le schéma d’entrée ne contient jamais que vos vrais arguments.
* `ctx.request_id` identifie la requête ; `ctx.request_context.lifespan_context` est ce que votre démarrage a produit avec yield.
* `await ctx.read_resource(uri)` permet à un outil de lire les propres ressources du serveur.
* `ctx.session` est le canal de retour vers le client : `send_tool_list_changed()` et ses sœurs lui demandent de récupérer à nouveau une liste que vous avez modifiée.
* Le rapport de progression et l’élicitation partent eux aussi du `Context` ; chacun a sa propre page.
Les paramètres que le modèle ne voit jamais, remplis par vos propres fonctions, sont les **[Dépendances](dependencies.md)**.

View file

@ -0,0 +1,173 @@
---
translation:
sections: [b0389403e98d25ad, e2cf58b43b285e86, a363e1a38e1a5971, 6cfac078feb18013, b4535bd61df337e6, e97ed44207f929fd]
tool: 1
---
# Dépendances {#dependencies}
Les arguments d’un outil (tool) viennent du modèle. Certaines valeurs ne devraient jamais en venir : un prix tiré de vos registres, une confirmation que seule une personne peut donner, tout ce que le modèle pourrait fausser en l’inventant.
Les **dépendances** sont des paramètres remplis par vos propres fonctions. Vous annotez le paramètre, vous nommez la fonction, et le SDK l’appelle avant l’exécution de votre outil.
## En déclarer une {#declare-one}
Enveloppez le type du paramètre dans `Annotated[...]` et ajoutez `Resolve(fn)` :
```python title="server.py" hl_lines="18-19 23"
--8<-- "docs_src/dependencies/tutorial001.py"
```
* `check_stock` est un **résolveur** : une simple fonction que le SDK exécute avant `reserve_book`, et dont la valeur de retour devient l’argument `stock`.
* Son paramètre `title` est l’argument `title` de l’outil lui-même, apparié **par nom**. Le résolveur voit exactement la valeur validée que verra le corps de l’outil.
* Le corps de l’outil part d’un `Stock` qui existe déjà. Pas de code de recherche dans l’outil, pas de préambule « et s’il manquait ? ».
!!! info
Si vous avez utilisé FastAPI, c’est `Depends`. Même geste, même raison : la fonction déclare
ce dont elle a besoin, le framework le fournit, et le câblage vit dans l’annotation de type.
### Invisible pour le modèle {#invisible-to-the-model}
Voici le schéma d’entrée que `tools/list` rapporte pour `reserve_book` :
```json
{
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"}
},
"required": ["title"],
"title": "reserve_bookArguments"
}
```
Une seule propriété. Comme le `Context` dans **[L’objet Context](context.md)**, un paramètre résolu est un contrat entre vous et le SDK : `stock` n’est pas dans le schéma, le modèle n’en entend jamais parler, et un client qui envoie quand même une valeur `stock` est ignoré. La valeur du résolveur est la seule que votre outil puisse recevoir.
Ce dernier point est l’essentiel. Un paramètre que le modèle ne peut pas fournir est un paramètre sur lequel le modèle ne peut pas se tromper.
### Essayer {#try-it}
Lancez le serveur avec le MCP Inspector :
```console
uv run mcp dev server.py
```
Le formulaire de `reserve_book` comporte un seul champ `title`. `stock` n’y figure nulle part. Appelez-le avec `Dune` :
```text
Reserved 'Dune' (6 copies left).
```
Le corps de l’outil n’a rien recherché : `check_stock` s’est exécuté d’abord, et le `Stock` qu’il a renvoyé est arrivé en argument. Essayez `Neuromancer` et le même résolveur remet un zéro à l’outil.
!!! tip
Vous pourriez simplement appeler `check_stock(title)` dans le corps de l’outil. Déclarez-le
comme dépendance quand la valeur mérite mieux qu’un appel de fonction utilitaire : chaque
outil qui a besoin du stock déclare le même paramètre, et le SDK exécute le résolveur au plus
une fois par appel, quel que soit le nombre d’outils qui le déclarent. Les sections suivantes
ajoutent le reste : des résolveurs qui dépendent les uns des autres, et des résolveurs qui
interrogent l’utilisateur.
## Dépendances de dépendances {#dependencies-of-dependencies}
Un résolveur peut déclarer ses propres dépendances, avec la même annotation :
```python title="server.py" hl_lines="22 29-30"
--8<-- "docs_src/dependencies/tutorial002.py"
```
* `estimate_delivery` dépend de `check_stock`. Le SDK exécute le graphe dans l’ordre : le stock d’abord, puis l’estimation, puis l’outil.
* `stock` comme `delivery` ont en fin de compte besoin de `check_stock`, mais celui-ci s’exécute **une fois par appel**. Une seule consultation de l’inventaire, deux consommateurs.
* Il n’y a rien à enregistrer. Le graphe, *ce sont* les annotations.
!!! check
Ne croyez pas le « une fois par appel » sur parole. Placez un `print` dans `check_stock` et
appelez `order_book` depuis l’Inspector : une ligne par appel. Deux consommateurs, une seule
consultation.
Le SDK analyse le graphe à l’enregistrement de l’outil, pas à son appel. Un paramètre qu’il ne sait pas classer — ni un `Context`, ni un `Resolve(...)`, ni le nom d’un argument de l’outil — et un cycle de résolveurs lèvent tous deux `InvalidSignature` au démarrage. Votre serveur échoue avant même qu’un client se connecte, avec le paramètre ou le résolveur fautif nommé dans l’erreur.
Les paramètres d’un résolveur se résolvent exactement comme ceux d’un outil : un autre `Resolve(...)`, les arguments de l’outil lui-même par nom, ou le `Context` — `ctx.headers`, l’objet du cycle de vie (lifespan), tout.
!!! warning
Sur les transports HTTP, le `Context` inclut `ctx.headers`. Les en-têtes sont des **entrées
fournies par le client**, comme n’importe quel argument d’outil : très bien pour une locale ou
un feature flag, jamais pour une identité. L’identité de l’appelant vient de votre couche
d’autorisation (**[Autorisation](../run/authorization.md)**), pas d’un en-tête que n’importe qui peut définir.
!!! tip
*Une fois par appel* veut dire exactement cela : le `tools/call` suivant exécute de nouveau
`check_stock`. Une ressource qui doit survivre à une requête — un pool de connexions à la base
de données, un client HTTP — a sa place dans **[Cycle de vie](lifespan.md)**, et un résolveur
peut l’atteindre via `ctx.request_context.lifespan_context`.
## Demander quand il le faut {#ask-when-you-must}
Un résolveur n’est pas obligé de connaître la réponse. Il peut renvoyer `Elicit(message, Model)` et le SDK interroge l’utilisateur — c’est la mécanique de l’**[Élicitation](elicitation.md)** (elicitation), pilotée pour vous :
```python title="server.py" hl_lines="26-32 39"
--8<-- "docs_src/dependencies/tutorial003.py"
```
* En stock : `confirm_backorder` renvoie directement un `Backorder`. **Pas de question, pas d’aller-retour.** L’utilisateur n’est interrompu que lorsque sa réponse compte.
* En rupture : le SDK envoie l’élicitation, valide la réponse par rapport à `Backorder`, et l’injecte. Votre résolveur ne touche jamais au protocole.
* L’outil lit `backorder.confirm` comme n’importe quel autre argument. Répondre **non** reste une réponse : l’élicitation est acceptée avec `confirm=False`, l’outil s’exécute, et aucune commande n’est passée. Poser la question est devenu une précondition, pas de la tuyauterie dans le corps de l’outil.
Et si l’utilisateur ne répond pas du tout — s’il décline la question, ou l’annule ?
!!! check
Lancez `order_book` pour `Neuromancer` et déclinez la question. Avec l’annotation écrite sous
la forme `Annotated[Backorder, Resolve(...)]`, le corps de l’outil ne s’exécute jamais ;
l’appel échoue avec un résultat d’erreur que le modèle peut lire :
```text
Error executing tool order_book: Resolver for parameter 'backorder' could not resolve: elicitation was decline
```
C’est le bon comportement par défaut pour une précondition : pas de réponse, pas de commande. Quand le refus est une issue que votre outil veut gérer — renoncer à la commande en attente mais suggérer tout de même un autre titre —, annotez plutôt `ElicitationResult[Backorder]` et l’outil reçoit l’issue complète accept/decline/cancel pour décider de la suite. **[Élicitation](elicitation.md)** montre cette forme, et tout le reste sur la manière de poser une question : les règles de schéma, les trois réponses, le côté client de la conversation.
!!! info
Le framework choisit le transport de la question d’après la version du protocole négociée ;
le code ci-dessus est identique dans les deux cas. En version **2026-07-28** et ultérieures,
la question voyage à l’intérieur d’un `tools/call` à plusieurs allers-retours
(multi-round-trip) — le serveur la renvoie, la fonction de rappel (callback)
`elicitation_callback` du client y répond, et le `Client` relance l’appel pour vous
(**[Requêtes à plusieurs allers-retours](multi-round-trip.md)**). En version **2025-11-25**
et antérieures, c’est une requête d’élicitation synchrone en cours d’appel. Chaque question
est posée exactement une fois par appel — une garantie qui porte sur la question, pas sur le
résolveur. Dans la forme à plusieurs allers-retours, n’importe quel résolveur peut s’exécuter
de nouveau chaque fois que l’appel reprend après une question ; le code placé avant un
`return Elicit(...)` s’exécute donc à chacun de ces tours, et la réponse enregistrée satisfait
alors la question répétée sans solliciter de nouveau l’utilisateur. Une réponse enregistrée
n’est consultée que lorsque le résolveur pose la question ; un résolveur qui répond *sans*
poser de question, comme `check_stock`, fournit toujours sa propre valeur calculée. Comme
chaque réponse est rattachée à sa question, un résolveur qui élicite doit dériver sa question
de façon déterministe à partir des arguments de l’outil et des réponses précédentes. Une
valeur générée à chaque appel (un identifiant issu d’un `default_factory`, un horodatage) est
recalculée à chaque tour et ne doit pas figurer dans une question à laquelle la réponse est
censée se lier. Une question construite à partir de données aussi volatiles fait paraître
périmée chaque réponse enregistrée ; le serveur la repose donc à chaque tour jusqu’à ce que
la limite de tours du client mette fin à l’appel.
## Interroger le client, pas l’utilisateur {#ask-the-client-not-the-user}
L’élicitation est l’une des trois questions qu’un résolveur peut poser, et le flux à plusieurs allers-retours n’en autorise aucune autre. Les deux autres s’adressent au **client** plutôt qu’à l’utilisateur : renvoyez `Sample(...)` pour faire exécuter un appel de LLM par le client (une requête `sampling/createMessage`), ou `ListRoots()` pour récupérer les racines (roots) actuelles du client. Aucune des deux n’a d’issue accept/decline ; le consommateur annote directement le type du résultat, `CreateMessageResult` (`CreateMessageResultWithTools` lorsque la requête porte `tools` ou `tool_choice`) ou `ListRootsResult` :
```python title="server.py" hl_lines="10-15 21"
--8<-- "docs_src/dependencies/tutorial004.py"
```
* Le framework les achemine exactement comme `Elicit` : à l’intérieur du `tools/call` à plusieurs allers-retours en version **2026-07-28**, via la requête autonome serveur->client en version **2025-11-25**. Une capacité non déclarée fait refuser l’appel avec une erreur de protocole `-32021` (`sampling`, `roots`, `elicitation` en mode formulaire ; `sampling.tools` lorsque la requête porte `tools` ou `tool_choice`).
* Tout ce que l’encadré d’information ci-dessus dit des questions s’applique tel quel : une requête `Sample` est rattachée à son résultat enregistré par son rendu exact ; construisez-la donc de façon déterministe à partir des arguments de l’outil et des réponses précédentes. Le client paie alors l’appel de LLM une fois par appel d’outil, pas une fois par tour. Le résultat enregistré voyage dans `request_state` pour le reste de l’appel, si bien qu’une complétion très volumineuse alourdit chaque aller-retour restant.
* Les *fonctionnalités* autonomes d’échantillonnage (sampling) et de racines sont obsolètes en version 2026-07-28 (SEP-2577). Les nouveaux serveurs qui ont besoin du modèle du client posent leur question via ce vecteur ; ceux qui n’en ont pas besoin devraient s’intégrer directement à un fournisseur de LLM. Les valeurs de `include_context` autres que `"none"` sont elles-mêmes obsolètes ; évitez-les.
## Récapitulatif {#recap}
* `Annotated[T, Resolve(fn)]` sur un paramètre d’outil : le SDK exécute `fn` et injecte sa valeur de retour.
* Un paramètre résolu est invisible pour le modèle et ne peut pas être fourni par un client. Les valeurs que le modèle ne doit pas inventer — prix, identités, permissions — ont leur place ici.
* Les paramètres d’un résolveur se résolvent de la même façon : le `Context`, un autre `Resolve(...)`, ou un argument de l’outil par nom. Le graphe exécute chaque résolveur au plus une fois par tour, quel que soit le nombre de ses consommateurs ; chaque question est posée exactement une fois, et n’importe quel résolveur peut s’exécuter de nouveau lorsqu’un appel reprend après une question.
* Les graphes incorrects échouent à l’enregistrement avec `InvalidSignature`, pas en cours d’appel.
* Renvoyez `Elicit(message, Model)` pour interroger l’utilisateur, seulement quand il le faut. Les annotations non enveloppées interrompent l’appel en cas de refus ; `ElicitationResult[T]` laisse l’outil décider de la suite.
* Renvoyez `Sample(...)` ou `ListRoots()` pour demander au client une complétion de LLM ou la liste des racines ; le résultat brut est injecté.
L’état que votre serveur construit une seule fois au démarrage, et la manière dont un gestionnaire (handler) y accède, c’est la page **[Cycle de vie](lifespan.md)**.

View file

@ -0,0 +1,190 @@
---
translation:
sections: [335ca2a0b266f003, d1ad562d3fe87bc0, 0bb1396c86daeba4, d1cb1235bb9ee267, 833179c09d239c83, e5d6dec2d2e655e8]
tool: 1
---
# Élicitation {#elicitation}
Un outil arrivé à mi-parcours de sa tâche et à qui il manque une seule réponse n’est pas obligé d’échouer.
L’**élicitation** (elicitation) lui permet de la demander. En plein appel d’outil, l’utilisateur reçoit une question, et sa réponse revient dans le même appel de fonction.
Il existe deux modes :
* **Mode formulaire** : vous avez besoin d’une valeur (une confirmation, une date, une quantité). Vous décrivez les champs, le client affiche le formulaire.
* **Mode URL** : vous avez besoin que l’utilisateur aille ailleurs (un écran de consentement OAuth, une page de paiement). Rien de ce qu’il y fait ne passe par le protocole.
Et il existe deux façons de demander. Celle à privilégier est un **résolveur** : vous accrochez la question à un paramètre, et le SDK la pose — sur n’importe quelle connexion, quelle que soit la génération de protocole que parle le client. La façon directe, `await ctx.elicit(...)`, est une requête du *serveur* vers le *client*, un canal qui n’existe que pour un client sur une connexion historique (version de spécification 2025-11-25 ou antérieure). Les deux figurent sur cette page ; commencez par le résolveur.
## Demander avec un résolveur {#ask-with-a-resolver}
Une question dont dépend tout l’outil — *êtes-vous sûr ? lequel des trois comptes correspondants ?* — peut être sortie du corps de l’outil et placée dans un **résolveur**, et le framework la pose pour vous.
Un paramètre annoté `Annotated[T, Resolve(fn)]` est rempli en exécutant `fn` avant le corps de l’outil. Le résolveur renvoie directement la valeur quand il la connaît déjà, ou renvoie `Elicit(...)` pour que le framework pose la question :
```python title="server.py" hl_lines="24-30 35-36"
--8<-- "docs_src/elicitation/tutorial004.py"
```
* `confirm_delete` lit par son nom l’argument `path` de l’outil lui-même, liste le dossier et **n’élicite que lorsqu’il le doit** — un dossier vide se résout en `Confirm(ok=True)` sans aucun aller-retour avec le client.
* `delete_folder` annote `ElicitationResult[Confirm]` : le framework injecte donc le résultat complet et l’outil traite chaque cas avec `match` : accepter et confirmer, accepter mais conserver (`ok=False`), décliner, annuler.
* Le paramètre `confirm` n’apparaît jamais dans le schéma d’entrée de l’outil — le client fournit `path`, le résolveur fournit `confirm`.
Annotez plutôt le modèle non enveloppé (`Annotated[Confirm, Resolve(confirm_delete)]`) quand l’outil n’a pas besoin de bifurquer : il reçoit le modèle en cas d’acceptation, et l’appel s’interrompt avec une erreur en cas de refus ou d’annulation.
Un résolveur fonctionne sur **toutes** les connexions. Pour un client sur une connexion historique, le SDK lui envoie directement la question ; sur une connexion **2026-07-28**, le SDK *renvoie* la question depuis l’appel, et la tentative suivante du client transporte la réponse. Votre résolveur ne voit jamais la différence ; ce qui se passe sous le capot, ce sont les **[Requêtes à plusieurs allers-retours](multi-round-trip.md)** (multi-round-trip).
Demander n’est qu’une des choses qu’un résolveur peut faire. Le mécanisme général — des dépendances qui calculent sans demander, des dépendances de dépendances, ce que le modèle peut et ne peut pas fournir — est décrit sur la page **[Dépendances](dependencies.md)**.
## Demander depuis l’intérieur de l’outil {#ask-from-inside-the-tool}
Un outil peut aussi s’arrêter au milieu de son propre corps et poser une question.
!!! warning
`ctx.elicit()` et `ctx.elicit_url()` sont des requêtes du *serveur* vers le *client* — un
canal qui n’existe que pour un client sur une connexion historique (version de spécification **2025-11-25**
ou antérieure). Sur une connexion **2026-07-28**, il n’y a pas de requêtes à l’initiative du serveur, donc
ces appels échouent. Un résolveur fonctionne sur les deux. Tous les détails sont dans
**[Versions du protocole](../protocol-versions.md)**.
`await ctx.elicit()` prend un message et un modèle Pydantic :
```python title="server.py" hl_lines="9-11 20-23 25"
--8<-- "docs_src/elicitation/tutorial001.py"
```
* Le paramètre **`Context`** est ce qui vous donne `ctx.elicit` ; n’importe quel outil peut en prendre un. Cet objet a sa propre page : **[L’objet Context](context.md)**.
* `AlternativeDate` est le **schéma** de la réponse que vous voulez.
* L’outil est `async def`. Il doit l’être : il s’arrête au milieu et attend une personne.
* Pour toute autre date, l’outil renvoie immédiatement. Il ne demande que lorsqu’il le doit.
* La date que l’utilisateur accepte repasse par `book_table` lui-même. Une réponse est une entrée comme une autre : une date de remplacement elle aussi complète fait l’objet d’une nouvelle question, au lieu d’être confirmée à l’aveugle.
### Ce que reçoit le client {#what-the-client-receives}
Le client reçoit votre message et, à côté, un JSON Schema généré à partir du modèle :
```json
{
"properties": {
"accept_alternative": {
"description": "Try another date?",
"title": "Accept Alternative",
"type": "boolean"
},
"date": {
"default": "2025-12-26",
"description": "Alternative date (YYYY-MM-DD)",
"title": "Date",
"type": "string"
}
},
"required": ["accept_alternative"],
"title": "AlternativeDate",
"type": "object"
}
```
Ce schéma, c’est le formulaire. `Field(description=...)` est le libellé ; une valeur par défaut préremplit le champ et le rend facultatif. C’est la même mécanique Pydantic vers JSON Schema que **[Outils](../servers/tools.md)** décrit pour les arguments d’un outil.
!!! warning
Un schéma d’élicitation n’est pas aussi expressif que le schéma d’entrée d’un outil. Des champs plats et primitifs
uniquement : `str`, `int`, `float`, `bool`, ou un `Literal` de chaînes (il devient un `enum`).
Mettez un modèle dans le modèle et `ctx.elicit` lève une exception avant que quoi que ce soit ne soit envoyé au client :
```text
TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition
```
Vous interrompez une personne en pleine tâche. Si la réponse a besoin d’imbrication, elle aurait dû être un
argument de l’outil.
### Les trois réponses {#the-three-answers}
`result.action` vous indique ce qu’a fait l’utilisateur, et il y a exactement trois possibilités :
* `"accept"` : il a soumis le formulaire. `result.data` est une instance de `AlternativeDate`, déjà validée.
* `"decline"` : il a dit non.
* `"cancel"` : il a écarté la question sans choisir.
`result.data` n’existe que sur `"accept"`, c’est pourquoi l’exemple vérifie `result.action` d’abord. Votre vérificateur de types impose cet ordre : après `result.action == "accept"`, `result.data` est un `AlternativeDate` ; avant, il n’y a pas de `.data` du tout.
Un refus n’est pas une erreur. L’outil décide de ce que signifie décliner (ici, pas de réservation) et répond normalement au modèle.
!!! tip
La réponse est validée par rapport à votre modèle avant que votre code ne la voie. Un client qui envoie
`"maybe"` pour un `bool` ne corrompt pas votre réservation : l’appel échoue avec une
erreur de non-conformité au schéma, votre `if` ne s’exécute jamais.
## Envoyer l’utilisateur vers une URL {#send-the-user-to-a-url}
Certaines choses ne doivent passer ni par le modèle ni par le client : identifiants, numéros de carte, consentement OAuth. Pour celles-là, vous ne demandez pas de données ; vous demandez à l’utilisateur d’aller quelque part :
```python title="server.py" hl_lines="10-14 23"
--8<-- "docs_src/elicitation/tutorial002.py"
```
* `ctx.elicit_url()` prend le message, l’**URL** à visiter et un `elicitation_id` que vous choisissez : n’importe quelle chaîne qui identifie cette élicitation au sein de votre serveur.
* Le résultat contient une action et rien d’autre. `"accept"` signifie que l’utilisateur a accepté d’ouvrir l’URL, **pas** qu’il a terminé ce qui se trouve de l’autre côté.
* Le paiement a lieu hors bande, entre le navigateur de l’utilisateur et votre prestataire de paiement. Aucun contenu ne revient jamais par MCP.
Regardez le second outil. Quand votre serveur apprend que le flux hors bande est terminé (un webhook, une interrogation périodique ; ici, c’est modélisé par un second outil), `ctx.session.send_elicit_complete(...)` envoie `notifications/elicitation/complete` avec le même `elicitation_id`. C’est ainsi que le client sait qu’il peut cesser d’afficher *« en attente du paiement… »*. Sans cela, le client ne peut que deviner.
## Côté client {#the-client-side}
Les serveurs demandent. Les clients répondent en passant une fonction de rappel (callback) **`elicitation_callback`** à `Client(...)` :
```python title="client.py" hl_lines="6-7 18"
--8<-- "docs_src/elicitation/tutorial003.py"
```
* Une seule fonction de rappel gère les deux modes. `params` est une union de `ElicitRequestFormParams` et `ElicitRequestURLParams` ; `isinstance` fait le branchement.
* Pour une URL, vous montrez `params.url` à l’utilisateur et renvoyez l’action qu’il a choisie. Jamais de `content`.
* Pour un formulaire, une vraie application affiche `params.requested_schema` et renvoie la saisie de l’utilisateur comme `content`. Celle-ci dit toujours oui avec une réponse toute faite, ce qui est exactement la fonction de rappel que vous voulez dans un test.
* Passer la fonction de rappel constitue aussi la **déclaration de capacité** : c’est ainsi que le serveur apprend que ce client peut être interrogé. Les autres choses auxquelles un client peut répondre pour un serveur se trouvent dans **[Fonctions de rappel du client](../client/callbacks.md)**.
!!! info
L’élicitation est une requête du *serveur* vers le *client*, et celles-ci n’existent que sur une
session à poignée de main (handshake) classique, c’est pourquoi ce client passe `mode="legacy"`.
Sur une connexion **2026-07-28**, un outil demande plutôt en *renvoyant* la question depuis l’appel ;
ce flux, ce sont les **[Requêtes à plusieurs allers-retours](multi-round-trip.md)**.
### Essayer {#try-it}
Démarrez le `server.py` en mode formulaire avec `ctx.elicit` (celui de `book_table`) sur Streamable HTTP (**[Exécuter votre serveur](../run/index.md)** donne la commande en une ligne), puis exécutez le `main()` du client et demandez à `book_table` le jour de Noël.
La fonction de rappel affiche la question qui lui a été envoyée :
```text
No tables for 2 on 2025-12-25. Would you like to try another date?
```
Elle répond avec `{"accept_alternative": True, "date": "2025-12-27"}`, et l’outil, qui attendait dans `await ctx.elicit(...)` pendant tout ce temps, termine la réservation :
```text
Booked a table for 2 on 2025-12-27.
```
Remplacez-le maintenant par le `server.py` en mode URL et pointez le même `main()` vers `pay_deposit` : la même fonction de rappel prend l’autre branche, affiche le lien de paiement, et l’outil revient avec *« Complete the payment in your browser. »* Un aller-retour, en plein appel, dans les deux sens.
!!! check
Retirez maintenant `elicitation_callback=` du `Client` et appelez de nouveau `book_table` pour le jour de Noël.
L’appel entier échoue avec une erreur de protocole :
```text
Elicitation not supported
```
Un client qui n’a enregistré aucune fonction de rappel n’a jamais déclaré la capacité `elicitation`, il n’y a donc
personne à qui demander. Votre outil n’a pas reçu de `"decline"` ; il a reçu une exception. Concevez en conséquence : chaque
élicitation a besoin d’une réponse sensée à la question « et si je ne peux pas demander ? ».
## Récapitulatif {#recap}
* Un paramètre annoté `Annotated[T, Resolve(fn)]` est rempli par un résolveur, qui renvoie `Elicit(...)` quand il doit demander. Cela fonctionne sur toutes les connexions.
* Le schéma est un modèle Pydantic plat : des champs primitifs uniquement, validés au retour.
* `result.action` vaut `"accept"`, `"decline"` ou `"cancel"` ; `result.data` n’existe qu’en cas d’acceptation.
* `await ctx.elicit(message, schema=Model)` demande depuis l’intérieur du corps de l’outil, et `await ctx.elicit_url(message, url, elicitation_id)` sert à tout ce qui ne doit pas passer par le modèle (`ctx.session.send_elicit_complete(elicitation_id)` indique que la partie hors bande est terminée). Les deux sont des requêtes du serveur vers le client : elles nécessitent que le client soit sur une connexion historique.
* Le client répond avec une seule `elicitation_callback`, en branchant sur le type des params ; l’enregistrer, c’est ce qui déclare la capacité.
* Sur une connexion 2026-07-28, le serveur renvoie la question au lieu de la pousser ; la même fonction de rappel est alimentée par les **[Requêtes à plusieurs allers-retours](multi-round-trip.md)**.
Tout ce qui se trouve sous ce retour (la boucle de réessai, la protection de `requestState`, le pilotage à la main) est dans **[Requêtes à plusieurs allers-retours](multi-round-trip.md)**.

View file

@ -0,0 +1,24 @@
---
translation:
sections: [424930166c4bc6f3]
tool: 1
---
# Dans votre gestionnaire {#inside-your-handler}
Les arguments d’un gestionnaire (handler) viennent du client. Tout ce qu’il peut lire *d’autre*, et tout ce qu’il peut faire pendant son exécution, se trouve ici.
Ce qu’il peut lire :
* **[L’objet Context](context.md)** est le seul paramètre supplémentaire que n’importe quel gestionnaire peut demander : la requête en cours, ses en-têtes, sa session, ainsi que les verbes de progression et de notification de changement.
* **[Les dépendances](dependencies.md)** sont des paramètres que le modèle ne voit jamais, renseignés par vos propres fonctions avec `Resolve`.
* **[Le cycle de vie](lifespan.md)** (lifespan) couvre l’état que votre serveur construit une seule fois au démarrage, et la façon dont un gestionnaire y accède via l’objet `Context`.
Ce qu’il peut faire pendant son exécution :
* Demander davantage d’informations à l’utilisateur avec **[l’élicitation](elicitation.md)** (elicitation), et les **[requêtes à plusieurs allers-retours](multi-round-trip.md)** (multi-round-trip), le mécanisme de la version 2026-07-28 qui la véhicule.
* Demander au client une complétion de LLM ou les dossiers de son espace de travail avec **[l’échantillonnage et les racines](sampling-and-roots.md)** (sampling et roots), obsolètes mais toujours pris en charge.
* Signaler la **[progression](progress.md)** d’une opération lente.
* Écrire des journaux (sur la sortie d’erreur standard, pour quiconque exploite le serveur) avec la **[journalisation](logging.md)**.
* Prévenir les clients abonnés que quelque chose a changé avec les **[abonnements](subscriptions.md)**.
Si vous n’avez pas encore enregistré de gestionnaire, commencez par **[Outils](../servers/tools.md)**. Chaque page de cette section suppose que vous en avez un.

View file

@ -0,0 +1,107 @@
---
translation:
sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53]
tool: 1
---
# Cycle de vie {#lifespan}
La plupart des vrais serveurs conservent quelque chose pendant toute leur durée de vie : un pool de connexions à la base de données, un client HTTP, un modèle chargé en mémoire.
Vous ne voulez pas le reconstruire à chaque appel, et vous voulez le fermer proprement. C’est à cela que sert le **cycle de vie** (lifespan).
## Un cycle de vie typé {#a-typed-lifespan}
Un cycle de vie est un `@asynccontextmanager` qui reçoit le serveur et produit avec `yield` **un seul objet**. Ce que vous produisez ainsi reste accessible à chaque gestionnaire (handler) aussi longtemps que le serveur tourne.
```python title="server.py" hl_lines="25-31 34 38 40"
--8<-- "docs_src/lifespan/tutorial001.py"
```
Lisez-le de bas en haut :
* `app_lifespan` connecte la `Database` **avant** le `yield` et la déconnecte **après**, dans un `finally`. C’est le démarrage et l’arrêt.
* Il produit un `AppContext`, une simple dataclass qui contient ce que vous avez initialisé. Un champ aujourd’hui, dix demain.
* `MCPServer("Bookshop", lifespan=app_lifespan)` est tout le câblage nécessaire.
* Dans l’outil, l’objet produit est `ctx.request_context.lifespan_context`.
Le cycle de vie s’exécute **une seule fois**. On y entre au démarrage du serveur (avant la première requête) et on en sort à l’arrêt du serveur. Toutes les requêtes entre les deux partagent le même `AppContext`.
!!! info
Si vous avez déjà écrit un `lifespan` FastAPI, vous connaissez déjà tout cela. Même décorateur, même `yield`, même `finally`.
### Ce que voit le modèle {#what-the-model-sees}
Rien de nouveau. `ctx` est un paramètre **Context** : le SDK l’injecte et il n’atteint jamais le schéma d’entrée :
```json
{
"type": "object",
"properties": {
"genre": {"title": "Genre", "type": "string"}
},
"required": ["genre"],
"title": "count_booksArguments"
}
```
`genre` est le seul argument que le modèle peut passer. Le cycle de vie, c’est l’affaire de votre serveur.
Les fonctions `@mcp.resource()` et `@mcp.prompt()` peuvent elles aussi prendre un paramètre `ctx`, annoté d’un simple `Context` pour une raison que la section suivante explique. Tout ce que transporte `ctx` est décrit dans **[L’objet Context](context.md)**.
### C’est réellement typé {#it-really-is-typed}
Regardez de nouveau l’annotation : `ctx: Context[AppContext]`.
Ce seul paramètre de type est la raison pour laquelle `ctx.request_context.lifespan_context` **est** un `AppContext` pour votre vérificateur de types. `.db` s’autocomplète ; `.dbb` est une erreur avant même que vous n’ayez lancé le serveur.
Écrivez un simple `Context` à la place et `lifespan_context` est typé `dict[str, Any]` : le vérificateur de types n’a aucun moyen de savoir ce que votre cycle de vie a produit. L’objet est toujours là à l’exécution ; vous avez perdu l’assistance.
!!! warning
`Context[AppContext]` est une écriture **réservée aux outils**. Mettez-la sur une fonction
`@mcp.resource()` ou `@mcp.prompt()` et chaque appel à ce gestionnaire échoue. Le client
reçoit une erreur en retour, et le journal du serveur montre pourquoi :
```text
Context is not available outside of a request
```
Dans les ressources et les prompts, écrivez simplement `ctx: Context`. L’objet produit par
votre cycle de vie reste `ctx.request_context.lifespan_context` à l’exécution ; vous renoncez
au paramètre de type, pas à l’objet.
!!! tip
Il y a toujours un cycle de vie. Si vous n’en passez pas, celui par défaut du SDK produit un
`dict` vide, si bien que `ctx.request_context.lifespan_context` vaut `{}`, jamais `None`.
Cette valeur par défaut explique aussi pourquoi un simple `Context` le type `dict[str, Any]`.
## Le voir se produire {#watch-it-happen}
« Le démarrage s’exécute avant la première requête » est le genre de phrase que vous ne devriez pas avoir à croire sur parole.
Réduisez le serveur à son cycle de vie : donnez à `Database` un indicateur `connected`, basculez-le dans `connect()` et `disconnect()`, et ajoutez un outil qui en rend compte.
```python title="server.py" hl_lines="11 14 17 25 44"
--8<-- "docs_src/lifespan/tutorial002.py"
```
`database` est défini au niveau du module pour une seule raison : pouvoir l’observer depuis *l’extérieur* du serveur.
!!! check
Trois moments, trois valeurs :
* Avant le démarrage du serveur, `database.connected` vaut `False`. Importer le module n’a rien connecté.
* Pendant qu’il tourne, appelez `database_status` et le résultat est `"connected"`.
* Arrêtez le serveur et le bloc `finally` s’exécute : `database.connected` vaut de nouveau `False`.
Le travail s’est fait exactement là où vous l’avez placé : autour du `yield`, pas à l’import et pas à chaque requête.
## Récapitulatif {#recap}
* `lifespan=` prend un `@asynccontextmanager` qui reçoit le serveur et produit avec `yield` un seul objet.
* Le code avant le `yield` est le démarrage. Le `finally` qui suit est l’arrêt.
* Il s’exécute une seule fois, autour de toute la vie du serveur, pas à chaque requête.
* Ce que vous produisez avec `yield` est `ctx.request_context.lifespan_context` dans chaque outil, ressource et prompt.
* `ctx: Context[AppContext]` rend cet accès entièrement typé dans les outils. Les ressources et les prompts prennent le simple `Context`.
* Pas de `lifespan=` signifie un `dict` vide, jamais `None`.
Un gestionnaire qui s’interrompt en plein appel pour demander à l’utilisateur quelque chose que lui seul connaît, c’est l’**[Élicitation](elicitation.md)**.

View file

@ -0,0 +1,86 @@
---
translation:
sections: [c93a3e1aefd77955, 7851abd5ec54393b, f49d1ca2f330f9cd, c03764bd9dfeef7b, 4a0391691a674ae4, 2df5cd279eabf9f5]
tool: 1
---
# Journalisation {#logging}
Journalisez depuis un outil comme vous le feriez depuis n’importe quelle autre fonction Python : avec la bibliothèque standard.
MCP possède une **capacité de journalisation** au niveau du protocole : un serveur pouvait envoyer ses messages de journal au client sous forme de notifications, via des méthodes de l’objet `Context`. La révision 2026-07-28 de la spécification **rend cette capacité obsolète sans la remplacer**, si bien que cette documentation ne l’enseigne pas. La liste complète de ce qui est obsolète, et de ce qu’il faut faire à la place, se trouve dans **[Fonctionnalités obsolètes](../deprecated.md)**.
Ce que vous faites à la place, c’est ce que vous faites dans tout autre programme Python : utiliser la bibliothèque standard.
## Un outil qui journalise {#a-tool-that-logs}
```python title="server.py" hl_lines="1 5 13"
--8<-- "docs_src/logging/tutorial001.py"
```
* `logging.getLogger(__name__)` vous donne un logger nommé d’après votre module. Créez-le une seule fois, en haut du fichier.
* Dans l’outil, vous appelez `logger.info(...)` comme dans n’importe quelle autre fonction. Rien à injecter, rien à `await`, rien de spécifique à MCP.
!!! check
Appelez l’outil et regardez le résultat complet :
```python
result.content # [TextContent(text="Found 3 books matching 'dune'.")]
result.structured_content # {'result': "Found 3 books matching 'dune'."}
```
La ligne de journal n’y figure nulle part. La journalisation est faite pour **vous**, la personne qui exploite le serveur. Le modèle
ne la voit jamais. Si le modèle doit lire quelque chose, renvoyez-le avec `return`.
## Où cela va {#where-it-goes}
Pour un serveur **stdio**, cette question compte plus que d’habitude. L’hôte a lancé votre serveur comme sous-processus et lit les messages MCP depuis son **stdout**. La sortie d’erreur standard est à vous.
La bibliothèque standard fait déjà ce qu’il faut : la sortie des journaux va vers `sys.stderr` par défaut. Vos lignes `logger.info(...)` arrivent dans le terminal (ou là où l’hôte collecte le stderr du sous-processus), et le flux du protocole reste propre.
!!! tip
N’utilisez pas `print()` dans un serveur stdio. `print` écrit sur **stdout**, et stdout appartient au protocole.
Pendant qu’il sert, le SDK redirige vers stderr ce qui est effectivement *vidé* (flush) sur stdout, de sorte que cela ne peut pas corrompre
la liaison ; mais dans un processus à tampon par blocs, un `print()` reste généralement non vidé dans le tampon de `sys.stdout`
jusqu’à ce que l’interpréteur le purge à la sortie, directement sur le flux du protocole. Même lorsqu’elle est redirigée,
la ligne arrive brute au milieu de la sortie des journaux, sans niveau, sans nom de logger et sans aucun moyen de la filtrer.
`logger.debug("got here")` demande le même effort d’une ligne et va au bon endroit.
## Le niveau {#the-level}
Vous n’avez pas à appeler `logging.basicConfig()` vous-même. La construction d’un `MCPServer` l’a déjà fait, avec un gestionnaire de journalisation pointé vers la sortie d’erreur standard, au niveau que vous passez via `log_level=` ; `MCPServer("Bookshop", log_level="DEBUG")` suffit donc pour voir vos lignes `logger.debug(...)`.
La valeur par défaut est `"INFO"`.
`logging.basicConfig()` ne remplace jamais des gestionnaires de journalisation qui existent déjà. Si vous configurez la journalisation vous-même avant de créer le serveur, votre configuration l’emporte.
## Essayer {#try-it}
Lancez le serveur avec le MCP Inspector :
```console
uv run mcp dev server.py
```
Appelez `search_books` depuis l’onglet **Tools**. L’Inspector vous montre le résultat : uniquement la valeur de retour. La ligne
```text
Searching for 'dune'
```
est partie vers la sortie d’erreur standard : le terminal, pas la liaison.
!!! info
Si ce que vous voulez vraiment, c’est du *traçage* (chaque requête, sa durée, son éventuel échec), vous
ne voulez pas des lignes de journal, vous voulez des spans. Votre serveur en émet déjà : le SDK trace chaque
message avec OpenTelemetry par défaut. Voir **[OpenTelemetry](../run/opentelemetry.md)**.
## Récapitulatif {#recap}
* La capacité de journalisation du protocole MCP est rendue obsolète par la spécification 2026-07-28 et n’est pas remplacée. Ne construisez rien dessus.
* `logger = logging.getLogger(__name__)` au niveau du module, `logger.info(...)` dans l’outil. C’est tout le modèle à suivre.
* La sortie des journaux n’atteint jamais le modèle. Seule la valeur que vous renvoyez avec `return` y parvient.
* La sortie d’erreur standard est à vous ; stdout appartient au protocole. Pendant qu’il sert, le SDK redirige vers stderr ce qui s’égare sur stdout et est vidé, mais un `print()` non vidé peut encore se déverser sur la liaison à la sortie, et les lignes redirigées arrivent sans étiquette ; utilisez `logging`, dont le gestionnaire vide chaque enregistrement.
* `MCPServer(..., log_level="DEBUG")` fixe le niveau, et une configuration de journalisation que vous avez faite au préalable est laissée telle quelle.
Prévenir les clients connectés que quelque chose a changé sur votre serveur (la liste des outils, une ressource), c’est l’affaire des **[Abonnements](subscriptions.md)**.

View file

@ -0,0 +1,192 @@
---
translation:
sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b]
tool: 1
---
# Requêtes à plusieurs allers-retours (multi-round-trip) {#multi-round-trip-requests}
Parfois, un outil ne peut pas terminer en un seul aller-retour. Il lui faut quelque chose que seul l’utilisateur détient : un choix, une confirmation, un identifiant d’accès.
Avant la version 2026-07-28, le serveur l’obtenait en **rappelant** le client : il ouvrait sa propre requête vers le client — une élicitation (elicitation), un appel d’échantillonnage (sampling) — au beau milieu du traitement de la requête d’origine. La spécification 2026-07-28 retire ce canal de retour (back-channel).
À la place, le serveur **renvoie un résultat**.
## Renvoyer, ne pas rappeler {#return-dont-call-back}
Le serveur répond à `tools/call` par un **`InputRequiredResult`** au lieu d’un `CallToolResult`. Deux de ses champs font le travail :
* **`input_requests`** : ce qu’il manque encore au serveur, sous la forme d’un dictionnaire dont les clés sont des noms choisis par le serveur. Chaque valeur est une `ElicitRequest`, une `CreateMessageRequest` ou une `ListRootsRequest`.
* **`request_state`** : un jeton opaque. Le client le renvoie tel quel lors de la nouvelle tentative. Votre serveur est le seul à le lire.
Le client satisfait chaque requête, puis appelle **à nouveau le même outil**, en transportant ses réponses dans `input_responses` et le jeton dans `request_state`. Le serveur dispose désormais de ce qui lui manquait et renvoie un `CallToolResult` normal.
C’est tout le protocole. Chaque étape est une requête ordinaire du client vers le serveur. Rien ne circule jamais dans l’autre sens.
## Côté serveur {#the-server-side}
Avec `@mcp.tool()`, vous construisez rarement cela à la main : déclarez une dépendance qui interroge l’utilisateur (`Elicit`), échantillonne le LLM du client (`Sample`) ou liste ses racines (roots) (`ListRoots`), et le SDK renvoie l’objet `InputRequiredResult` à votre place ; cette forme fait l’objet de la page **[Dépendances](dependencies.md)**. Les deux formes ne se mélangent pas : un appel ne dispose que d’un seul canal `input_responses`/`request_state`, si bien qu’un outil qui utilise des paramètres `Resolve(...)` ne peut pas en plus renvoyer un `InputRequiredResult` depuis son corps. Un retour `InputRequiredResult` déclaré est refusé à l’enregistrement (`InvalidSignature`), et un retour non déclaré fait échouer l’appel à l’exécution. La forme manuelle, c’est le `Server` **bas niveau**, dont le gestionnaire (handler) `on_call_tool` a le droit de renvoyer l’un ou l’autre type de résultat :
```python title="server.py" hl_lines="43-46"
--8<-- "docs_src/mrtr/tutorial001.py"
```
* `on_call_tool` est typé `-> CallToolResult | InputRequiredResult`. Renvoyer le second constitue toute l’API côté serveur.
* Au premier appel, `params.input_responses` vaut `None` : la garde se déclenche et le gestionnaire pose la question au lieu de répondre.
* Lors de la nouvelle tentative, le résultat `ElicitResult` envoyé par le client se trouve sous la **même clé** (`"region"`) que celle utilisée par le serveur dans `input_requests`.
Tout le reste de ce fichier (le `input_schema` explicite, le `CallToolResult` construit à la main) relève du `Server` bas niveau ordinaire, traité dans **[Le Server bas niveau](../advanced/low-level-server.md)**. Cette page n’ajoute que le second type de retour.
## Au-delà des outils {#beyond-tools}
`tools/call` n’a rien de particulier : en version 2026-07-28, un serveur peut répondre de la même façon à `prompts/get` et à `resources/read`. Sur `MCPServer`, une fonction `@mcp.prompt()` — ou une fonction `@mcp.resource()` **modèle** (template) — renvoie elle-même l’objet `InputRequiredResult` et lit les réponses de la nouvelle tentative dans le contexte :
```python title="server.py" hl_lines="20 22 24"
--8<-- "docs_src/mrtr/tutorial004.py"
```
* Le premier tour renvoie l’objet `InputRequiredResult`. Lors de la nouvelle tentative, `ctx.input_responses` contient les réponses sous les mêmes clés et la fonction renvoie son résultat ordinaire — ici des messages de prompt, du contenu de ressource pour une ressource modèle.
* Un `request_state` que vous définissez est scellé avant de franchir la liaison et vérifié à son retour en écho, comme tout le reste côté serveur ; **[Protéger `requestState`](#protecting-requeststate)** ci-dessous explique ce que le sceau vous apporte et quand vous devez configurer des clés.
* Une fonction `@mcp.tool()` peut renvoyer le résultat directement de la même façon, quand la forme par dépendance ne convient pas.
* Les fonctions `@mcp.resource()` statiques ne participent pas : elles ne prennent pas de `Context`, elles ne pourraient donc jamais lire la nouvelle tentative. Seules les ressources modèles peuvent poser une question.
* Les règles de génération ci-dessous s’appliquent telles quelles : renvoyer un `InputRequiredResult` sur une session antérieure à 2026 donne le même `-32603` que celui décrit par l’avertissement.
## Côté client {#the-client-side}
`Client` exécute la boucle pour vous.
Enregistrez les fonctions de rappel (callbacks) que le serveur pourrait solliciter (`elicitation_callback`, `sampling_callback`, `list_roots_callback`) et appelez l’outil. Quand un `InputRequiredResult` arrive, `Client` répartit chaque entrée de `input_requests` vers la fonction de rappel correspondante, relance l’appel avec les réponses et le `request_state` renvoyé en écho, et continue jusqu’à ce qu’un `CallToolResult` revienne :
```python title="client.py" hl_lines="11 12"
--8<-- "docs_src/mrtr/tutorial003.py"
```
* Cette `elicitation_callback` est celle-là même qu’aurait atteinte le `elicitation/create` du canal de retour d’un serveur antérieur à 2026. Il en va de même de `sampling_callback` pour `sampling/createMessage` et de `list_roots_callback` pour `roots/list` : en version 2026-07-28, les RPC autonomes du serveur vers le client ont disparu, mais les charges utiles `ElicitRequest` / `CreateMessageRequest` / `ListRootsRequest`, identiques, voyagent à l’intérieur de `input_requests` et sont distribuées aux trois mêmes fonctions de rappel. Un seul jeu de fonctions de rappel sert les deux générations.
* `call_tool` renvoie un simple `CallToolResult`. Les tours intermédiaires sont invisibles pour l’appelant.
* `get_prompt` et `read_resource` pilotent la même boucle.
!!! check
Omettez la fonction de rappel et la boucle échoue dès le premier tour : la fonction de rappel
de substitution du SDK répond à chaque élicitation par une erreur, et `call_tool` lève une
`MCPError` avec le message *« Elicitation not supported »*.
La boucle est bornée. `Client(..., input_required_max_rounds=10)` est le plafond par défaut ; un serveur qui continue de renvoyer des `InputRequiredResult` au-delà fait lever une exception à `call_tool`. Si un tour ne transporte que `request_state` sans `input_requests`, `Client` marque une courte pause (50 ms, doublés jusqu’à un plafond de 250 ms) avant de réessayer, de sorte qu’un serveur qui se contente de dire *« pas encore terminé »* ne soit pas sollicité en boucle.
### Piloter la boucle vous-même {#driving-the-loop-yourself}
La boucle automatique suffit pour un client à processus unique. Prenez plutôt la boucle en main quand :
* Votre client est **distribué** : le processus qui affiche la question à l’utilisateur n’est pas celui qui a appelé `call_tool`, c’est donc un autre worker qui émet la nouvelle tentative. `request_state` est le jeton persistant que vous transportez à travers cette frontière, via votre propre stockage, et `input_responses` est ce que l’autre côté renvoie avec lui.
* Vous voulez **inspecter** chaque tour : journaliser ou auditer chaque entrée de `input_requests`, refuser certains types de requêtes, ou appliquer votre propre temporisation entre les étapes.
* Vous voulez une borne en **temps réel** plutôt qu’en nombre de tours : enveloppez votre propre boucle dans `anyio.fail_after(...)` au lieu de compter sur `input_required_max_rounds`.
Descendez à la session sous-jacente, où `allow_input_required=True` vous remet directement l’union :
```python title="client.py" hl_lines="12 13 19"
--8<-- "docs_src/mrtr/tutorial002.py"
```
* `client.session.call_tool(..., allow_input_required=True)` élargit le type de retour à `CallToolResult | InputRequiredResult`. C’est le `isinstance` qui le resserre à nouveau.
* `request_state` est désormais entre vos mains. Notez-le entre deux étapes et la conversation peut reprendre depuis un processus tout neuf.
* Pour chaque entrée de `input_requests`, vous placez une `InputResponse` sous la **même clé** dans `input_responses`. `fulfil` est l’endroit où va votre interface utilisateur ; celle-ci code la réponse en dur.
* Même nom d’outil, mêmes `arguments`, à chaque étape. La nouvelle tentative, c’est l’appel d’origine exécuté de nouveau, pas une nouvelle méthode.
## Protéger `requestState` {#protecting-requeststate}
Tout ce qui précède traite `request_state` comme un écho, et sur la liaison ce n’est rien d’autre. Mais le client le conserve entre deux étapes (le noter pour le passer d’un processus à l’autre est précisément ce que la section précédente a approuvé), si bien que ce qui revient est une **entrée fournie par le client** : elle peut avoir été modifiée, avoir expiré, ou avoir été prélevée sur un tout autre appel. La spécification impose aux serveurs de protéger l’intégrité de cet état et de rejeter le tour quand la vérification échoue, dès lors que l’état peut influencer l’autorisation, l’accès aux ressources ou la logique métier.
`MCPServer` le protège par défaut. Chaque serveur scelle le `requestState` sortant et vérifie chaque écho — l’état des résolveurs comme l’état construit à la main — sous une clé générée au démarrage du processus. Vous ne configurez rien, vous écrivez du texte en clair et vous lisez du texte en clair ; la liaison ne transporte jamais qu’un jeton chiffré opaque.
La clé par défaut vit et meurt avec le processus ; c’est la seule chose que vous devez savoir avant de déployer au-delà d’un processus unique :
```python
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]))
```
* **La valeur par défaut (aucune configuration)** convient à un processus unique : stdio, ou exactement un worker HTTP. Une nouvelle tentative qui atterrit sur un autre worker, une autre instance derrière un répartiteur de charge, ou le même serveur après un redémarrage, est scellée sous une clé que ce processus ne possède pas — le client reçoit le rejet figé ci-dessous et doit recommencer le flux depuis le début.
* **`keys=[...]`** est obligatoire dès qu’une nouvelle tentative peut atteindre une **autre instance** (`uvicorn` à plusieurs workers, HTTP derrière répartiteur de charge) ou doit survivre aux redémarrages : chaque instance vérifie ce que n’importe quelle instance sœur a émis. Même mécanique, votre secret à la place d’un secret généré.
* Pour votre propre cryptographie, par exemple un KMS ou un service de jetons existant, passez `RequestStateSecurity(codec=...)` au lieu de `keys` ; **[Apporter votre propre cryptographie](#bring-your-own-crypto)** ci-dessous décrit le contrat.
### Ce que porte le sceau {#what-the-seal-carries}
Par défaut ou configuré, `requestState` sur la liaison est un jeton chiffré et authentifié. Votre code ne le voit jamais : gestionnaires et résolveurs écrivent du texte en clair et lisent du texte en clair (`ctx.request_state`) ; le SDK scelle à la sortie et vérifie à l’entrée. Au-delà de l’intégrité, chaque jeton est rattaché à :
* **Une fenêtre temporelle.** Chaque tour scelle de nouveau avec une échéance fraîche, si bien que `RequestStateSecurity(ttl=...)` (600 secondes par défaut) borne le temps de réflexion par tour, pas le flux entier.
* **Le principal authentifié.** Quand la requête porte un jeton d’accès OAuth validé par le SDK, l’état est rattaché au client, à l’émetteur et au sujet du jeton : un état émis pour un utilisateur échoue pour un autre, même quand les deux utilisateurs partagent un même client OAuth. Un vérificateur qui ne fournit aucun sujet réduit le rattachement à la seule identité du client, laquelle, avec des identifiants de client fondés sur une URL, est partagée par tous les utilisateurs de ce logiciel client. Quand l’authentification se termine en dehors du SDK (un proxy frontal), ou que le transport n’est pas authentifié, il n’y a aucun principal auquel se rattacher et cette vérification est inerte, sauf si `RequestStateSecurity(bind_principal=...)` en fournit un à partir de votre propre signal d’identité. Quels que soient les composants que votre vérificateur de jetons fournit, il doit les fournir de façon cohérente : un vérificateur qui inclut le sujet sur certaines requêtes et l’omet sur d’autres change de principal en plein flux, et les tours en cours sont rejetés.
* **La requête d’origine.** La méthode, le nom de l’outil ou du prompt (ou l’URI de la ressource), et une empreinte des arguments. Un jeton rejoué contre un autre outil, d’autres arguments ou une autre méthode échoue.
* **La question exacte posée.** Chaque réponse de résolveur est épinglée à la question rendue qui a été montrée au client, aussi bien au tour où elle arrive pour la première fois que lorsqu’une réponse enregistrée est réutilisée plus tard. Redéployez avec un message reformulé ou un schéma modifié et le serveur repose la question au lieu de consommer une réponse périmée. Le même épinglage joue aussi dans l’autre sens : dérivez les messages des arguments de l’outil, pas de données propres à chaque appel. Un message construit à partir d’un horodatage ou d’un taux en direct se rend différemment à chaque tour, si bien que chaque réponse enregistrée paraît périmée et que le serveur repose la question jusqu’à ce que la limite de tours du client mette fin à l’appel.
Tout cela est le travail du SDK, pas le vôtre, ni celui du codec si vous apportez le vôtre.
### Rotation des clés {#rotating-keys}
`keys[0]` scelle le nouvel état ; chaque clé de la liste vérifie. Une rotation sans interruption se fait en trois phases, chacune entièrement déployée avant la suivante :
```python
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
```
Ne promouvez jamais la clé d’émission en premier : émettre sous une clé qu’une instance ne sait pas encore vérifier fait tomber des tours en cours au milieu du déploiement.
Les clés sont limitées à un seul service. L’enveloppe scellée porte aussi le nom du serveur comme revendication d’audience, si bien qu’un jeton émis par un autre service qui se trouverait partager un secret est rejeté de toute façon. La revendication n’est distinctive que dans la mesure où le nom l’est : un serveur doté d’une politique explicite doit donc avoir un vrai nom ou définir `RequestStateSecurity(audience=...)` — un serveur sans nom lève une exception à la construction. `audience=` sert aussi aux topologies multi-services délibérées où un service doit accepter un état émis par un autre. (La valeur par défaut sans configuration est exemptée : sa clé ne quitte jamais le processus, la revendication d’audience n’a donc rien à ajouter.)
### Apporter votre propre cryptographie {#bring-your-own-crypto}
`RequestStateSecurity(codec=...)` accepte tout objet doté de `seal(bytes) -> str` et `unseal(str) -> bytes` qui lève `InvalidRequestState` pour tout jeton qu’il n’a pas émis. La forme classique est le chiffrement d’enveloppe adossé à un KMS : vous déchiffrez une clé de données une seule fois au démarrage et gardez la cryptographie par jeton en local :
```python title="server.py" hl_lines="12 26-27 34-35 38"
--8<-- "docs_src/mrtr/tutorial005.py"
```
Le TTL, le rattachement au principal et le rattachement à la requête ne sont **pas** l’affaire du codec : le SDK les inscrit dans la charge utile avant `seal` et les revérifie après `unseal`, pour chaque codec. Les seules obligations d’un codec sont l’intégrité (altéré signifie lever une exception) et, idéalement, la confidentialité.
### Quand la vérification échoue {#when-verification-fails}
Chaque échec entrant, qu’il s’agisse d’un jeton altéré, expiré, rejoué contre une autre requête ou un autre principal, ou scellé sous une clé que ce serveur ne connaît pas, reçoit la même réponse :
```json
{"code": -32602, "message": "Invalid or expired requestState"}
```
Un seul message figé pour toutes les causes, afin que la liaison ne révèle jamais quelle vérification a échoué ; la vraie raison va dans le journal du serveur. Chaque `requestState` entrant sur `tools/call`, `prompts/get` et `resources/read` est vérifié, y compris celui qui arrive pour un gestionnaire qui n’émet jamais d’état. Le rejet le plus courant en pratique n’est pas un attaquant — c’est la clé par défaut, locale au processus, qui rencontre une nouvelle tentative antérieure à un redémarrage ou venue d’une autre instance ; le client relance le flux, et `keys=[...]` est le correctif quand cela compte.
### État construit à la main {#hand-built-state}
Un `request_state` que vous définissez vous-même (en renvoyant `InputRequiredResult` depuis une fonction d’outil, de prompt ou de modèle de ressource) est scellé et vérifié par la même mécanique que l’état des résolveurs, sans aucune modification de code : écrivez du texte en clair, lisez du texte en clair, et chaque rattachement ci-dessus s’applique.
La seule chose que le SDK ne peut pas épingler pour vous, même configuré, c’est l’identité de la question : il ne sait pas à laquelle de *vos* questions appartient une réponse présente dans votre état. Si vous stockez des réponses indexées par question, incluez votre propre identifiant de question dans l’état et vérifiez-le lors de la nouvelle tentative.
Le `Server` bas niveau est le niveau sans rien de fourni d’office : contrairement à `MCPServer`, rien n’est scellé tant que vous n’ajoutez pas vous-même la frontière, et votre `request_state` franchit la liaison exactement tel qu’écrit jusqu’à ce que vous le fassiez. L’activation en une ligne est montrée dans **[Le Server bas niveau](../advanced/low-level-server.md#the-other-handlers)**.
## Un résultat de la version 2026-07-28 {#a-2026-07-28-result}
`InputRequiredResult` n’existe qu’en version de protocole **2026-07-28**. Le `Client(server)` en mémoire la négocie pour vous ; sur la liaison, `mode="auto"` la découvre. Une fois connecté, `client.protocol_version` vous dit ce que vous avez obtenu.
!!! warning
Une session antérieure à 2026 n’a nulle part où mettre un `InputRequiredResult`. Renvoyez-en
un depuis votre gestionnaire sur une connexion `mode="legacy"` et l’exécuteur ne peut pas le
sérialiser dans la version négociée ; le client reçoit en retour une erreur `-32603`
*« Handler returned an invalid result »*. Un serveur qui sert les deux générations doit vérifier
`ctx.protocol_version` avant d’y recourir.
!!! info
L’**élicitation en mode URL** emprunte exactement ce mécanisme sur une connexion 2026. L’entrée
dans `input_requests` est une `ElicitRequest` dont les params sont `ElicitRequestURLParams` ;
l’utilisateur termine le flux hors bande et votre client relance l’appel. Même boucle, aucune
nouvelle API. La moitié serveur haut niveau se trouve dans **[Élicitation](elicitation.md)**.
## Récapitulatif {#recap}
* En version 2026-07-28, un serveur qui a besoin d’une entrée en cours d’appel **renvoie** un `InputRequiredResult`. Il n’ouvre jamais de requête vers le client.
* `input_requests` est ce dont il a besoin. `request_state` est un jeton de reprise opaque que seul le serveur lit.
* `Client` exécute la boucle de nouvelles tentatives pour vous : enregistrez `elicitation_callback` / `sampling_callback` / `list_roots_callback` et `call_tool` renvoie un simple `CallToolResult`. `input_required_max_rounds` (10 par défaut) la borne.
* Pour inspecter ou persister les tours, utilisez `client.session.call_tool(..., allow_input_required=True)` et prenez vous-même en main la boucle `while isinstance(result, InputRequiredResult)`.
* Avec `@mcp.tool()`, une dépendance qui interroge l’utilisateur produit ce résultat pour vous (**[Dépendances](dependencies.md)**) ; le `Server` **bas niveau** est la forme manuelle.
* Les prompts et les ressources participent aussi : une fonction `@mcp.prompt()` ou une fonction `@mcp.resource()` modèle renvoie elle-même l’objet `InputRequiredResult` et lit `ctx.input_responses` lors de la nouvelle tentative.
* `requestState` revient sous forme d’entrée fournie par le client, donc `MCPServer` le scelle par défaut — l’état des résolveurs comme l’état construit à la main — sous une clé locale au processus ; les déploiements multi-instances passent `RequestStateSecurity(keys=[...])` (ou un codec personnalisé) pour que chaque instance puisse vérifier ce qu’une instance sœur a émis. Le sceau rattache chaque jeton à une fenêtre temporelle, à la requête d’origine et au principal authentifié lorsque la requête porte une authentification validée par le SDK ou que `bind_principal=` fournit votre propre signal d’identité (**[Protéger `requestState`](#protecting-requeststate)**).
C’est le mécanisme qui remplace l’échantillonnage à l’initiative du serveur et le reste du canal de retour de type push ; voir **[Fonctionnalités obsolètes](../deprecated.md)**.

View file

@ -0,0 +1,112 @@
---
translation:
sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007]
tool: 1
---
# Progression {#progress}
Un outil qui met trente secondes et ne dit rien pendant trente secondes a l’air cassé.
Les **notifications de progression** règlent cela. L’outil indique où il en est ; le client décide quoi en afficher : une barre, une roue qui tourne, une ligne de journal.
## La signaler depuis l’outil {#report-it-from-the-tool}
Prenez un paramètre **`Context`** et appelez `report_progress` :
```python title="server.py" hl_lines="8 11"
--8<-- "docs_src/progress/tutorial001.py"
```
Trois arguments, et c’est vous qui décidez de leur sens :
* `progress` : où vous en êtes. La spécification exige qu’il **augmente** à chaque signalement ; ne répétez jamais une valeur et ne revenez jamais en arrière.
* `total` : la quantité totale, si vous la connaissez. Optionnel.
* `message` : une ligne lisible par un humain à propos de *cette* étape. Optionnel.
`ctx` est injecté grâce à son annotation de type et le modèle ne le voit jamais : le schéma d’entrée de `import_catalog` a une seule propriété, `urls`. La page **[L’objet Context](context.md)** est entièrement consacrée à cet objet ; la progression est l’une des choses qu’il vous apporte.
## L’écouter depuis le client {#listen-for-it-from-the-client}
Le client active la fonctionnalité **appel par appel**, en passant `progress_callback=` à `call_tool` :
```python title="client.py" hl_lines="7 16"
import anyio
from mcp import Client
from server import mcp
async def show(progress: float, total: float | None, message: str | None) -> None:
print(f"{message} ({progress}/{total})")
async def main() -> None:
async with Client(mcp) as client:
result = await client.call_tool(
"import_catalog",
{"urls": ["https://example.com/a.json", "https://example.com/b.json"]},
progress_callback=show,
)
print(result.structured_content)
anyio.run(main)
```
La fonction de rappel (callback) est une fonction `async` qui prend exactement ce que le serveur a signalé : `progress`, `total`, `message`.
!!! info
`Client(mcp)` se connecte directement à l’objet serveur, en mémoire : c’est le même client que celui sur lequel repose la page **[Tests](../get-started/testing.md)**. `progress_callback` est le même paramètre quel que soit le transport qu’utilise le `Client` ; le *timing* que vous allez observer est celui de la connexion en mémoire. Elle exécute votre fonction de rappel de façon synchrone, si bien que chaque signalement arrive avant que `call_tool` ne renvoie. Sur un vrai transport, les notifications font la course avec le résultat, et une fonction de rappel lente peut encore être en cours d’exécution après le retour de `call_tool`.
### Essayer {#try-it}
Placez `client.py` à côté de `server.py` et lancez-le :
```console
python client.py
```
```text
Imported https://example.com/a.json (1/2)
Imported https://example.com/b.json (2/2)
{'result': 'Imported 2 records.'}
```
Chaque `await ctx.report_progress(...)` côté serveur est devenu un appel à `show` côté client, dans l’ordre, et les deux lignes se sont affichées **avant** que `call_tool` ne renvoie. La progression n’est pas empaquetée dans le résultat ; elle est diffusée pendant que l’outil travaille encore.
!!! warning
`progress_callback` appartient à l’**appel**, pas au `Client`. Il n’existe aucun argument de constructeur pour cela, parce que des appels différents veulent des fonctions de rappel différentes : l’un pilote une barre de téléchargement, le suivant une ligne de journal.
!!! check
Maintenant, supprimez `progress_callback=show` et relancez :
```text
{'result': 'Imported 2 records.'}
```
Aucune erreur, aucun avertissement, même résultat. `report_progress` **ne fait rien quand l’appelant n’a pas demandé la progression** : vous signalez donc sans condition et n’avez jamais à vous demander si quelqu’un écoute.
## Quand vous ne connaissez pas le total {#when-you-dont-know-the-total}
`total` sert quand vous connaissez le dénominateur. Souvent, ce n’est pas le cas : vous videz un flux, parcourez un curseur, téléchargez quelque chose sans en-tête de longueur.
Omettez-le :
```python title="server.py" hl_lines="20"
--8<-- "docs_src/progress/tutorial002.py"
```
La fonction de rappel reçoit `total=None`. Un client peut toujours montrer une *activité* (« 3 importés jusqu’ici… ») mais il ne peut pas afficher de pourcentage. N’inventez pas un total pour obtenir une plus jolie barre.
!!! tip
`progress` n’a pas à compter quelque chose de précis. Octets, lignes, pages : choisissez l’unité que l’utilisateur reconnaîtrait, et ne promettez qu’un `total` que vous pouvez tenir.
## Récapitulatif {#recap}
* `await ctx.report_progress(progress, total=None, message=None)` depuis n’importe quel outil qui prend un `Context`.
* Le client passe `progress_callback=` à `call_tool` : appel par appel, jamais sur le `Client`.
* La fonction de rappel est `async (progress, total, message) -> None` et se déclenche pendant que l’outil s’exécute encore.
* Sans fonction de rappel sur l’appel, `report_progress` ne fait rien. Signalez sans condition.
* Omettez `total` quand vous ne le connaissez pas ; la fonction de rappel reçoit `None`.
La progression est ce qu’un outil en cours d’exécution montre à l’*utilisateur*. Les lignes qu’il journalise pour *vous*, la personne qui exploite le serveur, passent par un autre canal : la **[journalisation](logging.md)**.

View file

@ -0,0 +1,51 @@
---
translation:
sections: [5c82b20cbd65ded0, 9dc22632be79a533, 1fb8f452e990c456, 42666ab914ff0cb1, c4e0cb3667fd5ff9]
tool: 1
---
# Échantillonnage et racines {#sampling-and-roots}
Un gestionnaire (handler) peut demander deux choses de plus au client connecté : une complétion produite par le modèle du client lui-même — l’**échantillonnage** (sampling) — et les dossiers de l’espace de travail du client — les **racines** (roots).
Les deux fonctionnent toujours, sur toutes les versions du protocole que le SDK parle. Mais lisez l’avertissement avant de concevoir quoi que ce soit autour d’elles :
!!! warning "Rendus obsolètes par la spécification 2026-07-28"
L’échantillonnage et les racines sont obsolètes depuis la version `2026-07-28` ([SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2577)). Ils restent pleinement fonctionnels et demeurent dans la spécification pendant au moins douze mois avant de pouvoir être supprimés, mais les nouvelles implémentations ne devraient pas s’appuyer dessus. Les migrations suggérées : intégrez-vous directement à l’API de votre fournisseur de LLM au lieu de l’échantillonnage, et transmettez les répertoires via des paramètres d’outil, des URI de ressource ou la configuration du serveur au lieu des racines. La liste complète pour le SDK se trouve dans **[Fonctionnalités obsolètes](../deprecated.md)**.
## Échantillonnage : emprunter le modèle du client {#sampling-borrow-the-clients-model}
Un résolveur renvoie `Sample(...)` et l’outil reçoit la complétion, par le même mécanisme de dépendances qui exécute `Elicit` dans **[Dépendances](dependencies.md)** :
```python title="server.py" hl_lines="10-15 19"
--8<-- "docs_src/sampling_and_roots/tutorial001.py"
```
* `Sample(messages, max_tokens=...)` reprend les paramètres de `sampling/createMessage`. La valeur injectée est le `CreateMessageResult` du client ; passez `tools` ou `tool_choice` et elle devient un `CreateMessageResultWithTools`.
* Le client doit avoir déclaré la capacité `sampling` (`sampling.tools` si vous passez `tools` ou `tool_choice`). S’il ne l’a pas fait, l’appel échoue avec une erreur de protocole `-32021` au lieu d’envoyer une requête que le client ne peut pas traiter. Une session antérieure à 2026 sans canal de retour (back-channel) échoue avec son erreur habituelle d’absence de canal de retour, puisqu’il n’y a rien sur quoi envoyer.
* En version `2026-07-28`, la requête est acheminée dans le flux à plusieurs allers-retours (multi-round-trip) (**[Requêtes à plusieurs allers-retours](multi-round-trip.md)**) ; en version `2025-11-25`, c’est une requête autonome adressée au client. Le code est le même dans les deux cas, mais gardez à l’esprit la règle des requêtes à plusieurs allers-retours : la requête doit être rendue à l’identique d’une tentative à l’autre, construisez-la donc uniquement à partir des arguments de l’outil et d’autres données stables.
* Ne touchez pas à `include_context` : les valeurs autres que `"none"` sont elles-mêmes obsolètes (SEP-2596) et exigent une capacité que presque aucun client ne déclare.
## Racines : où cela doit-il aller ? {#roots-where-should-this-go}
Les racines sont les dossiers sur lesquels le client indique que le serveur peut opérer. Ce sont des indications à titre informatif, pas un mécanisme de contrôle d’accès. Un résolveur renvoie `ListRoots()` :
```python title="server.py" hl_lines="10-11 15"
--8<-- "docs_src/sampling_and_roots/tutorial002.py"
```
* Le `ListRootsResult` injecté contient une liste de `Root` : un URI `file://` et un nom d’affichage facultatif.
* Le garde-fou est le même que pour l’échantillonnage : sans capacité `roots` déclarée, l’appel échoue avec `-32021` au lieu d’envoyer la requête.
De l’autre côté de la liaison, le client répond aux deux requêtes avec les fonctions de rappel (callbacks) dont il dispose déjà : `sampling_callback` et `list_roots_callback`, décrites dans **[Fonctions de rappel du client](../client/callbacks.md)**.
## Sur les connexions de génération 2025 {#on-2025-era-connections}
`ctx.session.create_message(...)` et `ctx.session.list_roots()` existent toujours pour le code qui pilote la session directement. Elles ne fonctionnent que là où un canal de retour existe (connexions de génération 2025 qui ne sont pas sans état), et les appeler déclenche un avertissement d’obsolescence. Les marqueurs de résolveur ci-dessus sont la forme prise en charge : ils choisissent le mode d’acheminement d’après la version négociée et n’émettent pas d’avertissement.
## Récapitulatif {#recap}
* Renvoyez `Sample(...)` ou `ListRoots()` depuis un résolveur ; l’outil reçoit le `CreateMessageResult` ou le `ListRootsResult` comme n’importe quelle autre dépendance.
* Le client doit déclarer la capacité correspondante, sinon l’appel échoue avec `-32021` au lieu qu’une requête soit envoyée.
* Les deux fonctionnalités sont obsolètes en version `2026-07-28` : pleinement fonctionnelles pour l’instant, inadaptées aux nouvelles conceptions. Préférez les API des fournisseurs à l’échantillonnage et les paramètres explicites aux racines.
Indiquer l’avancement d’un outil lent : **[Progression](progress.md)**.

View file

@ -0,0 +1,152 @@
---
translation:
sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883]
tool: 1
---
# Abonnements {#subscriptions}
Le catalogue d’un serveur n’est pas figé. Des outils apparaissent à l’exécution, et le contenu derrière l’URI d’une ressource change.
**Les abonnements** sont le moyen par lequel un client en est informé. Le client envoie une seule requête `subscriptions/listen`, et la réponse à cette requête *est* le flux : elle reste ouverte et transporte les notifications de changement que le client a demandées.
## Publier depuis l’outil {#publish-it-from-the-tool}
Votre part se résume à une ligne : publier le changement.
```python title="server.py" hl_lines="20 32"
--8<-- "docs_src/subscriptions/tutorial001.py"
```
* `await ctx.notify_resource_updated("board://sprint")` atteint chaque flux ouvert abonné à cet URI. Personne d’autre.
* `await ctx.notify_tools_changed()` atteint chaque flux qui a demandé les changements de la liste d’outils. Un client qui la reçoit appelle de nouveau `tools/list`, et voit désormais `sprint_report`.
* Les méthodes sœurs sont `notify_prompts_changed()` et `notify_resources_changed()`.
* Pas d’abonnés, pas de travail. Publier sur un serveur inactif est sans effet, vous ne vérifiez donc jamais si quelqu’un écoute. Vous indiquez ce qui a changé.
`MCPServer` sert `subscriptions/listen` pour vous. Les obligations sur la liaison (l’accusé de réception comme première trame, le filtrage par flux, l’identifiant d’abonnement sur chaque trame) sont l’affaire du SDK.
!!! check
Sur la liaison, un flux dont le filtre nommait `board://sprint` ressemble à ceci après l’exécution de `complete_task` :
```json
{"method": "notifications/subscriptions/acknowledged",
"params": {"notifications": {"resourceSubscriptions": ["board://sprint"]}, "_meta": {"io.modelcontextprotocol/subscriptionId": "listen-1"}}}
{"method": "notifications/resources/updated",
"params": {"uri": "board://sprint", "_meta": {"io.modelcontextprotocol/subscriptionId": "listen-1"}}}
```
Notez ce que la mise à jour ne transporte *pas* : le tableau. Chaque trame porte l’identifiant JSON-RPC de la requête listen sous `_meta`, et cet identifiant est l’identifiant d’abonnement. C’est le client qui le crée : le `Client` Python utilise des chaînes comme `"listen-1"` ; d’autres clients peuvent utiliser des entiers.
## Seulement ce qui a été demandé {#only-what-was-asked-for}
Le filtre est un contrat. Un flux qui a demandé les changements de la liste d’outils et un URI de ressource reçoit ces deux types et rien d’autre. Publiez un changement de prompt et ce flux reste silencieux.
`MCPServer` compare les URI de ressource comme des chaînes exactes, si bien qu’un flux qui a nommé `board://sprint` n’entend rien à propos de `board://sprint/tasks/1`. La spécification permet à un serveur de signaler un changement sur une sous-ressource d’un URI abonné ; `MCPServer` ne le fait jamais, mais les clients sont conçus pour s’y attendre.
Deux choses que le flux n’est *pas* :
* **Ce n’est pas un journal de relecture.** Un flux interrompu est perdu, et les événements publiés pendant que personne n’était connecté ne sont pas mis en file d’attente. Les clients rouvrent l’écoute et récupèrent de nouveau les données.
* **Ce n’est pas le chemin 2025.** Les clients qui ont appelé `resources/subscribe` sont servis par `ctx.session.send_resource_updated(uri)`. Les méthodes `notify_*` n’atteignent que les flux `subscriptions/listen`.
## Décider qui peut observer {#deciding-who-may-watch}
Par défaut, chaque type et chaque URI demandés sont honorés : n’importe quel appelant peut observer n’importe quel URI que vous publiez. Rien ne consulte votre gestionnaire (handler) de lecture, car personne ne lit — un appelant que votre gestionnaire `files://{name}` refuserait peut tout de même ouvrir un flux sur `files://payroll.csv` et apprendre qu’il a changé, et quand. Il n’apprend jamais le contenu, et il ne peut pas sonder ce qui existe, car un URI inconnu est honoré lui aussi et ne se déclenche tout simplement jamais. La faille est étroite mais réelle : mettez donc un contrôle en place avant de publier des URI propres à chaque utilisateur depuis un serveur multi-locataire.
Le contrôle est un middleware. Il voit la requête `subscriptions/listen` avant que le SDK n’en accuse réception et refuse lorsque l’appelant demande quoi que ce soit qu’il n’a pas le droit de lire :
```python title="server.py" hl_lines="19-26 29"
--8<-- "docs_src/subscriptions/tutorial006.py"
```
* `ctx.params` est la requête brute ; le middleware la valide donc lui-même en `SubscriptionsListenRequestParams` et lit le filtre demandé par le client.
* Le refus est une `MCPError` levée avant `call_next(ctx)` : le client reçoit cette erreur et aucun flux, et la connexion continue. Gardez le message uniforme, sans nommer d’URI, afin qu’un refus ne confirme jamais quels URI sont protégés.
* Une seule fonction `can_access(user, uri)` répond aux deux questions. Le gestionnaire de ressource la pose sur `resources/read` ; le middleware la pose sur `subscriptions/listen`. Remplacez la table par une base de données ou votre système RBAC et les deux restent synchronisés.
* La décision vaut pour toute la durée de vie du flux. Il n’y a pas de nouvelle vérification par événement ; si l’accès d’un appelant peut expirer en cours de flux (un jeton qui expire), mettez donc fin à la connexion de cet appelant à ce moment-là.
Le contrat complet du middleware, y compris ce qu’il enveloppe d’autre et pourquoi il est marqué comme provisoire, se trouve sur **[Middleware](../advanced/middleware.md)**.
## Côté client {#the-client-end}
Voici un client de l’autre côté de ce flux, qui suit le tableau :
```python title="client.py" hl_lines="15"
--8<-- "docs_src/subscriptions/tutorial003.py"
```
Entrer dans `client.listen(...)` envoie la requête et attend votre accusé de réception : le flux est donc actif quand le bloc commence, et chaque événement typé est un signal pour récupérer de nouveau les données, jamais une charge utile. C’est tout le contrat, en un seul écran. Tout le reste concernant le côté client a sa propre page : observer à côté d’un traitement principal, fins de flux et réouverture de l’écoute. Voir **[Abonnements](../client/subscriptions.md)** sous *Clients*.
## Passer à l’échelle au-delà d’un seul processus {#scaling-past-one-process}
Les publications voyagent de votre gestionnaire vers les flux ouverts via un `SubscriptionBus`. Le bus par défaut est en mémoire : un processus, et tous les flux qu’il contient. C’est la bonne réponse jusqu’au jour où vous exécutez des réplicas derrière un répartiteur de charge, car le flux d’un client est alors épinglé à un réplica, et une publication sur un autre réplica doit l’atteindre.
Cette jointure est à vous d’implémenter : deux méthodes au-dessus de votre backend pub/sub.
```python
from collections.abc import Callable
from redis.asyncio import Redis
from mcp.server.mcpserver import MCPServer
from mcp.server.subscriptions import ServerEvent # SubscriptionBus is a Protocol: no base class
class RedisSubscriptionBus:
def __init__(self, redis: Redis) -> None:
self._redis = redis
self._listeners: dict[object, Callable[[ServerEvent], None]] = {}
async def publish(self, event: ServerEvent) -> None:
await self._redis.publish("mcp-events", encode(event)) # to every replica
def subscribe(self, listener: Callable[[ServerEvent], None]) -> Callable[[], None]:
token = object()
self._listeners[token] = listener
def unsubscribe() -> None:
self._listeners.pop(token, None)
return unsubscribe
mcp = MCPServer("Sprint Board", subscriptions=RedisSubscriptionBus(redis))
```
`encode` est à vous, tout comme la tâche de lecture sur chaque réplica qui décode les messages entrants et appelle chaque écouteur enregistré. Les écouteurs sont synchrones, ne doivent pas lever d’exception et s’exécutent sur la boucle d’événements du serveur.
Le bus transporte des valeurs `ServerEvent` typées, quatre petites dataclasses, jamais du JSON-RPC. Le marquage, le filtrage et le cycle de vie des flux restent dans le SDK, si bien qu’une implémentation de bus ne peut pas casser le protocole. Elle ne peut que déplacer des événements entre processus.
Pour publier en dehors d’une requête, construisez le bus vous-même afin d’en garder la référence. `MCPServer` en construit un en interne lorsque vous ne passez rien, et ne l’expose pas.
```python
from mcp.server.subscriptions import InMemorySubscriptionBus, ToolsListChanged
bus = InMemorySubscriptionBus()
mcp = MCPServer("Sprint Board", subscriptions=bus)
async def tools_reloaded() -> None:
await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere
```
## La composition bas niveau {#the-low-level-composition}
Sur le `Server` bas niveau, rien n’est précâblé, et les mêmes pièces s’assemblent en trois lignes :
```python title="server.py" hl_lines="8-9 47"
--8<-- "docs_src/subscriptions/tutorial002.py"
```
* Le bus vous appartient, vous y publiez donc directement : `await bus.publish(ResourceUpdated(uri=...))`. Placez-le là où vos gestionnaires peuvent l’atteindre : la portée du module ici, le cycle de vie (lifespan) dans une application plus grande.
* `ListenHandler(bus)` est le même gestionnaire que celui qu’enregistre `MCPServer`, et `on_subscriptions_listen=` est un emplacement de gestionnaire ordinaire. Mettez votre propre callable dans cet emplacement pour une sémantique différente, et les obligations de la spécification vous reviennent : accuser réception d’abord, marquer chaque trame avec l’identifiant d’abonnement, ne rien livrer en dehors du filtre.
* `ListenHandler.close()` termine proprement chaque flux ouvert. Chacun reçoit le résultat de la requête listen comme dernière trame, ce qui est la manière pour la spécification de dire que le serveur a mis fin à l’abonnement délibérément. Elle rend la main avant que ces flux n’aient fini de se vider : laissez-leur donc un instant avant de démonter le transport. Sans cela, les flux se terminent lorsque le client se déconnecte.
## Récapitulatif {#recap}
* Un client s’inscrit avec une seule requête `subscriptions/listen`, et la réponse est le flux. La prise en charge est intégrée.
* Vous publiez avec `ctx.notify_*`, et le SDK se charge du marquage, du filtrage et du cycle de vie.
* Les événements sont des signaux, pas des charges utiles. Les deux extrémités récupèrent de nouveau les données.
* Le côté client, c’est `async with client.listen(...)` : tous les détails sont dans **[Abonnements](../client/subscriptions.md)** sous *Clients*.
* Sur le `Server` bas niveau, vous assemblez vous-même les mêmes pièces : un bus, `ListenHandler(bus)`, l’emplacement `on_subscriptions_listen`.
* Passer à l’échelle horizontalement signifie implémenter `SubscriptionBus`, deux méthodes, et le passer via `MCPServer(subscriptions=...)`.
Exécuter le serveur qui sert tout cela, derrière un réplica ou vingt, c’est **[Déployer et passer à l’échelle](../run/deploy.md)**.