docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
50 changed files with 89737 additions and 54 deletions
121
i18n/fr/pages/advanced/apps.md
Normal file
121
i18n/fr/pages/advanced/apps.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
|
||||
tool: 1
|
||||
---
|
||||
# MCP Apps {#mcp-apps}
|
||||
|
||||
Une **MCP App** est un outil doté d’une interface : en plus de ses données, l’outil désigne un document HTML que l’hôte affiche comme surface interactive.
|
||||
|
||||
Deux parties, toujours deux parties :
|
||||
|
||||
1. **Un outil** qui fait le travail et renvoie des données, comme n’importe quel autre outil.
|
||||
2. **Une ressource `ui://`** contenant le HTML que l’hôte affiche pour lui.
|
||||
|
||||
L’outil porte une référence `_meta.ui.resourceUri` vers la ressource. L’hôte la récupère avec `resources/read`, l’affiche dans une **iframe isolée (sandbox)** et pousse le résultat de l’outil dans cette iframe via `postMessage`. Votre serveur n’envoie ni ne reçoit jamais de messages `ui/*` : ce trafic circule entre l’hôte et l’iframe. Vous servez un outil et un document HTML ; l’hôte se charge de la mise en scène.
|
||||
|
||||
Le SDK fournit cela sous la forme de l’extension intégrée `Apps` (`io.modelcontextprotocol/ui`). Si les [extensions](extensions.md) sont nouvelles pour vous, parcourez d’abord cette page. Une minute, puis revenez.
|
||||
|
||||
## Une horloge avec un cadran {#a-clock-with-a-face}
|
||||
|
||||
```python title="server.py" hl_lines="19 22 30 32"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
Quatre étapes :
|
||||
|
||||
* `Apps()` : une seule instance contient vos outils liés à une interface et leurs ressources.
|
||||
* `@apps.tool(resource_uri="ui://clock/app.html")` : un outil ordinaire, plus le marquage `_meta.ui.resourceUri`. Tout ce que `@mcp.tool()` accepte (name, title, description, …) est transmis tel quel.
|
||||
* `apps.add_html_resource("ui://clock/app.html", CLOCK_HTML)` : la ressource correspondante, servie en `text/html;profile=mcp-app`. C’est ce type MIME exact qui indique à un hôte « ceci est une app, affichez-la ».
|
||||
* `MCPServer("clock", extensions=[apps])` : vous activez l’extension. Le serveur annonce désormais `io.modelcontextprotocol/ui` sous `capabilities.extensions`.
|
||||
|
||||
Le HTML lui-même écoute le `postMessage` de l’hôte et affiche le résultat. Pour de vraies applications, utilisez dans votre HTML le SDK navigateur officiel [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps). Il vous donne `ontoolresult`, `callServerTool`, `getHostContext` et `onhostcontextchanged` au lieu d’événements de message bruts.
|
||||
|
||||
## Dégradation gracieuse {#graceful-degradation}
|
||||
|
||||
Tous les clients n’affichent pas les apps. La spécification dit sans détour ce que cela implique pour vous :
|
||||
|
||||
> Les outils **DOIVENT** renvoyer un tableau `content` significatif même lorsqu’une interface est disponible.
|
||||
|
||||
Le modèle lit `content` ; l’iframe est pour les humains. Un hôte capable d’afficher une interface transmet quand même le résultat textuel au modèle, et un client purement textuel ne reçoit *que* cela. Le schéma canonique est donc : un outil, deux réponses. Regardez à nouveau `get_time` :
|
||||
|
||||
```python title="server.py" hl_lines="23-27"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
`client_supports_apps(ctx)` ne vaut `True` que lorsque le client a déclaré l’extension `io.modelcontextprotocol/ui` **et** listé `text/html;profile=mcp-app` dans ses paramètres `mimeTypes`. Le champ est obligatoire, donc un client qui l’omet ne compte pas. C’est exactement ce que déclare `main()` dans le même fichier : la moitié client de la négociation, et la réponse riche revient.
|
||||
|
||||
!!! warning
|
||||
Ne renvoyez jamais un texte de substitution comme `"[Rendered UI]"` pour seul contenu. Si le texte de repli est inutile, l’outil est inutile pour tout client purement textuel et pour le modèle lui-même. Écrivez la phrase.
|
||||
|
||||
## Verrouiller l’iframe {#locking-the-iframe-down}
|
||||
|
||||
C’est le côté ressource qui porte les métadonnées de sécurité : ce que l’iframe peut charger, les permissions du navigateur qu’elle souhaite, la façon dont elle aimerait être encadrée :
|
||||
|
||||
```python title="server.py" hl_lines="9 19-22"
|
||||
--8<-- "docs_src/apps/tutorial002.py"
|
||||
```
|
||||
|
||||
`csp` et `permissions` sont des **demandes adressées à l’hôte**, pas un comportement du serveur. L’hôte construit à partir d’elles la Content-Security-Policy et la Permissions-Policy de l’iframe, et il peut refuser. Faites de la détection de fonctionnalités dans votre JS plutôt que de supposer l’accord acquis.
|
||||
|
||||
`ResourceCsp`, champ par champ (nom Python, clé sur la liaison, ce que l’hôte en fait) :
|
||||
|
||||
| Python | Liaison (`_meta.ui.csp`) | Contrôle |
|
||||
|---|---|---|
|
||||
| `connect_domains` | `connectDomains` | `connect-src` : où `fetch`/XHR peuvent aller |
|
||||
| `resource_domains` | `resourceDomains` | `img-src`, `style-src`, … : fichiers statiques |
|
||||
| `frame_domains` | `frameDomains` | `frame-src` : iframes imbriquées |
|
||||
| `base_uri_domains` | `baseUriDomains` | `base-uri` : ce vers quoi `<base>` peut pointer |
|
||||
|
||||
`ResourcePermissions` : chaque champ demande une permission du navigateur pour l’iframe.
|
||||
|
||||
| Python | Liaison (`_meta.ui.permissions`) |
|
||||
|---|---|
|
||||
| `camera` | `camera` |
|
||||
| `microphone` | `microphone` |
|
||||
| `geolocation` | `geolocation` |
|
||||
| `clipboard_write` | `clipboardWrite` |
|
||||
|
||||
!!! note
|
||||
La CSP et les permissions vivent sur la **ressource**, jamais sur l’outil. Les métadonnées d’outil de la spécification n’ont pas d’emplacement pour elles, et les hôtes les ignorent à cet endroit. Le SDK rend l’erreur impossible à exprimer : `@apps.tool()` n’a tout simplement pas de paramètre `csp`.
|
||||
|
||||
### Visibilité {#visibility}
|
||||
|
||||
`visibility=["app"]` sur un outil dit « ceci existe pour l’iframe, pas pour le modèle » :
|
||||
|
||||
* `"model"` : le modèle peut l’appeler.
|
||||
* `"app"` : l’iframe peut l’appeler (via `callServerTool`).
|
||||
* Omis : les deux, ce qui est la valeur par défaut.
|
||||
|
||||
Le filtrage est le travail de **l’hôte**. Votre serveur liste les outils réservés à l’app dans `tools/list` comme les autres ; l’hôte les cache au modèle. Ne filtrez pas côté serveur.
|
||||
|
||||
## Les règles que le SDK fait respecter {#the-rules-the-sdk-enforces}
|
||||
|
||||
Toutes échouent au démarrage, pas en production :
|
||||
|
||||
* Un `resource_uri` ou un URI de ressource qui n’est pas `ui://...` lève une `ValueError` au moment de la décoration ou de l’enregistrement.
|
||||
* Un outil lié à un URI **sans ressource enregistrée correspondante** lève une `ValueError` lorsque `MCPServer(extensions=[apps])` consomme l’extension. Un outil qui annonce du HTML répondant 404 sur `resources/read` est une erreur de configuration, donc le serveur refuse de se construire.
|
||||
* `meta={"ui": ...}` sur `@apps.tool()` lève une `ValueError`. Le décorateur est propriétaire de `_meta["ui"]` ; exprimez-le avec `resource_uri=` et `visibility=`. Les autres clés `meta=` se fusionnent sans problème à côté.
|
||||
|
||||
Ni le SDK TypeScript ext-apps ni FastMCP ne détectent ces cas aujourd’hui ; nous préférons que vous le découvriez avant qu’un hôte ne le fasse.
|
||||
|
||||
## Au-delà du HTML inline {#beyond-inline-html}
|
||||
|
||||
`add_html_resource` couvre le cas courant : une chaîne de HTML. Pour tout le reste, HTML sur disque ou contenu généré, construisez la ressource vous-même et transmettez-la :
|
||||
|
||||
```python title="server.py" hl_lines="12 18"
|
||||
--8<-- "docs_src/apps/tutorial003.py"
|
||||
```
|
||||
|
||||
`add_resource` renseigne le type MIME `text/html;profile=mcp-app` quand la ressource n’en définit pas explicitement, et rejette une incohérence explicite : une ressource `ui://` sous tout autre type MIME est une ressource qu’aucun hôte n’affichera.
|
||||
|
||||
!!! tip
|
||||
Vous ciblez un hôte d’avant la disponibilité générale qui lit encore la clé plate obsolète `_meta["ui/resourceUri"]` ? Fusionnez-la vous-même : `@apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"})`. L’objet `ui` imbriqué est la forme prévue par la spécification ; la clé plate est en voie de disparition.
|
||||
|
||||
## Le voir en action {#see-it-run}
|
||||
|
||||
Le scénario `apps` dans `examples/stories/`, c’est cette page sous forme de paire exécutable : un serveur avec un outil horloge lié à une interface et un client qui négocie Apps, lit le `_meta.ui.resourceUri` de l’outil, récupère le HTML et appelle l’outil.
|
||||
|
||||
```bash
|
||||
uv run python -m stories.apps.client
|
||||
```
|
||||
172
i18n/fr/pages/advanced/extensions.md
Normal file
172
i18n/fr/pages/advanced/extensions.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
---
|
||||
translation:
|
||||
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
|
||||
tool: 1
|
||||
---
|
||||
# Extensions {#extensions}
|
||||
|
||||
Une **extension** est un ensemble de comportements MCP, activable sur demande, regroupé derrière un seul identifiant.
|
||||
|
||||
Côté serveur, elle peut apporter des outils (tools), des ressources et de nouvelles méthodes de requête, et elle peut envelopper `tools/call`. Côté client, elle peut revendiquer des formes de résultat `tools/call` supplémentaires et observer des notifications propres à un éditeur. Chaque côté s’annonce sous son propre `capabilities.extensions`, et rien ne change pour quiconque ne l’a pas demandé. C’est le contrat ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)), et il a une règle d’or : **les extensions sont désactivées par défaut**.
|
||||
|
||||
## Utiliser une extension {#using-an-extension}
|
||||
|
||||
Passez des instances à la construction :
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/extensions/tutorial001.py"
|
||||
```
|
||||
|
||||
C’est fait. Le serveur annonce désormais `io.modelcontextprotocol/ui` sous `capabilities.extensions` et sert tout ce que l’extension apporte.
|
||||
|
||||
`Apps` est l’extension de référence intégrée, et elle a sa propre page : **[MCP Apps](apps.md)**.
|
||||
|
||||
!!! note
|
||||
Les extensions sont figées à la construction. Il n’existe pas de `add_extension` à appeler plus tard : la table des capacités d’un serveur ne devrait pas changer pendant que des clients y sont connectés.
|
||||
|
||||
La table des capacités transite par `server/discover`, qui est un chemin **2026-07-28**. Une poignée de main (handshake) `initialize` historique n’a aucun endroit où la placer, donc un client historique ne voit tout simplement pas l’extension. Concevez en conséquence : une extension *enrichit* un serveur, elle ne doit pas être la seule manière de le rendre utilisable.
|
||||
|
||||
## Écrire la vôtre {#writing-your-own}
|
||||
|
||||
Dérivez `Extension` et ne redéfinissez que ce dont vous avez besoin. Chaque méthode a une valeur par défaut.
|
||||
|
||||
### L’identifiant {#the-identifier}
|
||||
|
||||
```python
|
||||
--8<-- "docs_src/extensions/tutorial002.py"
|
||||
```
|
||||
|
||||
L’identifiant est une chaîne `vendor-prefix/name` qui suit la grammaire des clés `_meta` de la spécification : des libellés séparés par des points (chacun commence par une lettre et se termine par une lettre ou un chiffre), une barre oblique, puis le nom. Il est validé **au moment où la classe est définie**, de sorte qu’une faute de frappe n’attend pas le démarrage d’un serveur :
|
||||
|
||||
```text
|
||||
TypeError: Stamps.identifier must be a `vendor-prefix/name` string
|
||||
(reverse-DNS prefix required), got 'stamps'
|
||||
```
|
||||
|
||||
Utilisez comme préfixe un domaine que vous contrôlez. `io.modelcontextprotocol/*` est réservé aux extensions spécifiées par le projet MCP lui-même.
|
||||
|
||||
### Apporter des outils {#contributing-tools}
|
||||
|
||||
La plus petite extension utile, c’est un outil et une table de paramètres :
|
||||
|
||||
```python title="server.py" hl_lines="17 19-20 22-23 26"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
* `tools()` renvoie des `ToolBinding`. Le serveur enregistre chacun exactement comme si vous aviez appelé `mcp.add_tool(...)` vous-même : même génération de schéma, même injection de `Context`, tout à l’identique.
|
||||
* `settings()` est la valeur annoncée sous `capabilities.extensions["com.example/stamps"]`. Renvoyez `{}` (la valeur par défaut) pour annoncer l’extension sans paramètres.
|
||||
* L’extension ne reçoit jamais le serveur. Elle déclare ses contributions sous forme de données ; `MCPServer` les consomme. Il n’y a pas de `self.server` à modifier.
|
||||
|
||||
Et `main()` en est la preuve, un client en mémoire branché directement sur `mcp` :
|
||||
|
||||
```python title="server.py" hl_lines="29-34"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
### Servir vos propres méthodes {#serving-your-own-methods}
|
||||
|
||||
Une extension peut enregistrer de **nouvelles méthodes de requête** : ses propres verbes, servis à côté de ceux de la spécification :
|
||||
|
||||
```python title="server.py" hl_lines="16-22 31 40-48"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
* `SearchParams` dérive de `RequestParams`, si bien que l’enveloppe `_meta` de 2026 est analysée de façon uniforme et que votre gestionnaire (handler) reçoit des paramètres validés, jamais un dict brut. Bornez ce que le client contrôle : `Field(ge=1, le=100)` rejette un `limit` absurde avant que votre code n’alloue quoi que ce soit pour lui.
|
||||
* `require_client_extension(ctx, EXTENSION_ID)` est le garde-fou : un client qui n’a pas déclaré l’extension reçoit l’erreur `-32021` (capacité client obligatoire manquante), avec la charge utile `requiredCapabilities` lisible par machine que la spécification demande.
|
||||
* `protocol_versions=frozenset({"2026-07-28"})` épingle la méthode à une seule version de la liaison. Dans toute autre version, le client reçoit `METHOD_NOT_FOUND`, exactement comme si la méthode n’y existait pas. Pour ce client, elle n’existe pas.
|
||||
|
||||
Les méthodes sont **strictement additives**. Le SDK le fait respecter à la construction, pas à l’exécution :
|
||||
|
||||
* Un `MethodBinding` pour une méthode définie par la spécification (`tools/list`, `completion/complete`…) lève une `ValueError` lors de la construction du binding. Les verbes de base appartiennent au serveur.
|
||||
* Deux extensions qui lient la même méthode lèvent une exception quand la seconde s’enregistre. Laisser la dernière écriture l’emporter, c’est ainsi que des plugins se corrompent mutuellement ; nous ne faisons pas cela.
|
||||
* Un ensemble `protocol_versions` vide lève aussi une exception : une méthode qui ne peut jamais être servie est un bogue, pas une configuration.
|
||||
|
||||
### Le côté client {#the-client-side}
|
||||
|
||||
Le `main()` du même fichier raconte toute l’histoire côté client, ses deux moitiés :
|
||||
|
||||
```python title="server.py" hl_lines="54-58"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
* `Client(..., extensions=[advertise(EXTENSION_ID)])` déclare l’extension. Les déclarations deviennent `ClientCapabilities.extensions` : sur une connexion 2026-07-28, la table voyage dans l’enveloppe `_meta` de chaque requête, donc le serveur la voit sur **chaque** requête ; sur une connexion historique, elle transite par la poignée de main `initialize`. Le code serveur ne s’en soucie pas : `require_client_extension(ctx, ...)` et `ctx.session.check_client_capability(...)` lisent la bonne source dans les deux cas.
|
||||
* Les méthodes propres à un éditeur descendent d’un niveau, vers `client.session.send_request(...)` ; `Client` n’acquiert de méthodes de premier rang que pour les verbes de la spécification. `send_request` accepte n’importe quelle sous-classe de `Request`, donc la requête de l’éditeur passe telle quelle.
|
||||
|
||||
### Intercepter `tools/call` {#intercepting-toolscall}
|
||||
|
||||
Le seul hook d’interception. Redéfinissez `intercept_tool_call` pour observer, court-circuiter ou opposer un veto à un appel d’outil :
|
||||
|
||||
```python title="server.py" hl_lines="17-24"
|
||||
--8<-- "docs_src/extensions/tutorial005.py"
|
||||
```
|
||||
|
||||
* `params` est le `CallToolRequestParams` validé : vous obtenez `params.name` et `params.arguments` sans toucher au JSON brut. C’est aussi lui qui décide quel appel d’outil s’exécute : passer un contexte réécrit à `call_next` change ce que le gestionnaire observe sur `ctx`, pas l’invocation de l’outil. La réécriture de requêtes au niveau de la liaison relève du [Middleware](middleware.md).
|
||||
* `call_next(ctx)` exécute le reste de la chaîne et renvoie le résultat du gestionnaire. Renvoyez-le tel quel (observer), renvoyez autre chose (remplacer) ou levez une `MCPError` (refuser). Ce que vous renvoyez est sérialisé comme n’importe quel résultat de gestionnaire, y compris l’estampille d’identité `serverInfo` de la génération 2026, si bien qu’un intercepteur qui court-circuite ne produit jamais de réponse anonyme ou hors schéma.
|
||||
* Avec plusieurs extensions, les intercepteurs s’imbriquent dans l’ordre d’enregistrement : la première extension de `extensions=[...]` est la plus externe.
|
||||
* L’implémentation par défaut laisse passer sans rien faire, et un serveur dont les extensions ne redéfinissent jamais ce hook conserve le gestionnaire `tools/call` nu, intact. Vous ne payez pas pour ce que vous n’utilisez pas.
|
||||
|
||||
Le hook enveloppe `tools/call` et rien d’autre. Pour ce qui concerne chaque message, utilisez le [Middleware](middleware.md). Il est fait pour cela.
|
||||
|
||||
## Utiliser une extension client {#using-a-client-extension}
|
||||
|
||||
Une **extension client**, c’est le même contrat vu du côté consommateur : un ensemble de comportements côté client derrière un seul identifiant. Passez des instances à `Client(extensions=[...])` et appelez les outils normalement :
|
||||
|
||||
```python title="client.py" hl_lines="66-68"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
`call_tool("buy", ...)` renvoie un simple `CallToolResult`, comme tout autre appel. Ce que l’extension a changé : le serveur peut désormais répondre à `buy` par une **forme de résultat** `receipt` au lieu d’un résultat final, et `Receipts` la termine (ici en échangeant le reçu via un appel de suivi) avant que `call_tool` ne renvoie. Rien ne bouge au point d’appel.
|
||||
|
||||
Retirez l’extension et rien de tout cela n’existe : le garde-fou du serveur refuse un client qui ne l’a pas déclarée (erreur -32021), et une forme revendiquée venant d’un serveur qui saute le garde-fou échoue à la validation, exactement comme la spécification l’exige pour un `resultType` non reconnu. Désactivé par défaut, aux deux bouts de la liaison.
|
||||
|
||||
Pour annoncer un identifiant **sans aucun** comportement côté client (le serveur filtre sur la capacité, le client ne fait rien, comme dans le client de recherche ci-dessus), utilisez `advertise()` :
|
||||
|
||||
```python
|
||||
from mcp.client import advertise
|
||||
|
||||
client = Client(mcp, extensions=[advertise("com.example/search")])
|
||||
```
|
||||
|
||||
## Écrire une extension client {#writing-a-client-extension}
|
||||
|
||||
Dérivez `ClientExtension` et ne redéfinissez que ce dont vous avez besoin. Trois types de contributions, chacun avec une valeur par défaut : `settings()`, `claims()` et `notifications()`.
|
||||
|
||||
```python title="client.py" hl_lines="17-18 43-44 46-47"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
* L’identifiant suit la même grammaire que celui du serveur, validé au moment où la classe est définie.
|
||||
* `claims()` renvoie des `ResultClaim` : une étiquette de liaison, le modèle qui l’analyse et le résolveur qui la termine. Le modèle doit épingler l’étiquette avec `result_type: Literal["receipt"]` et ne doit pas dériver des types de résultat de base du verbe ; les deux sont vérifiés à la construction du claim. Les champs d’éditeur comme `receipt_token` voyagent tels quels sur la liaison : une forme substituée parvient au client à l’identique.
|
||||
* Le résolveur reçoit le modèle analysé et un `ClaimContext` ; `ctx.session` est le même point d’accès public que `client.session`, donc les appels de suivi sont des appels de session ordinaires. Il renvoie le `CallToolResult` normal du verbe.
|
||||
* `settings()` est la valeur annoncée sous `ClientCapabilities.extensions[identifier]`, lue une fois à la construction de `Client`.
|
||||
|
||||
`notifications()` déclare les notifications serveur d’éditeur à observer :
|
||||
|
||||
```python
|
||||
def notifications(self) -> Sequence[NotificationBinding[Any]]:
|
||||
return [NotificationBinding(method="notifications/receipts", params_type=ReceiptEvent, handler=self.on_receipt)]
|
||||
```
|
||||
|
||||
Le gestionnaire reçoit des paramètres validés un par un, dans l’ordre de distribution. Il observe ; il ne peut ni opposer de veto ni répondre.
|
||||
|
||||
Deux règles discrètes. Les claims ne sont actifs que sur les connexions 2026-07-28, et l’annonce des capacités les suit : sur une connexion historique, les claims se dissolvent et l’identifiant disparaît de l’annonce avec eux, si bien que le client n’annonce jamais une extension dont il rejetterait les formes. Et lorsque vous voulez la forme revendiquée elle-même plutôt que le résolveur, appelez `client.session.call_tool(..., allow_claimed=True)` ; sans ce drapeau, une forme revendiquée qui atteint un appelant au niveau session lève `UnexpectedClaimedResult`.
|
||||
|
||||
### Verbes d’extension {#extension-verbs}
|
||||
|
||||
Les méthodes de requête propres à une extension n’ont besoin d’aucun enregistrement côté client. Un type de requête d’éditeur dérive de `mcp.types.Request` et passe par `client.session.send_request`, comme dans [Servir vos propres méthodes](#serving-your-own-methods). Un ajout : lorsqu’une clé des paramètres doit transiter par l’en-tête `Mcp-Name` (des spécifications d’extension comme tasks l’exigent pour leurs verbes), le type de requête déclare `name_param` :
|
||||
|
||||
```python title="client.py" hl_lines="22-25 46-47"
|
||||
--8<-- "docs_src/extensions/tutorial007.py"
|
||||
```
|
||||
|
||||
La session reflète `params["jobId"]` dans `Mcp-Name` sur chaque chemin d’envoi, et une valeur manquante échoue bruyamment au lieu d’omettre silencieusement un en-tête obligatoire.
|
||||
|
||||
## Ce qu’une extension ne peut pas faire {#what-an-extension-cannot-do}
|
||||
|
||||
La surface de contribution est **fermée** à dessein. Côté serveur : paramètres, outils, ressources, méthodes, un intercepteur `tools/call`. Côté client : paramètres, claims de résultat, bindings de notification. Une extension ne peut pas :
|
||||
|
||||
* **Atteindre l’hôte.** Elle déclare des données ; elle ne détient aucune référence au serveur ni au client.
|
||||
* **Remplacer le comportement de base.** Les méthodes de la spécification et les étiquettes de résultat de base sont rejetées à la construction (`initialize` est purement et simplement réservé par le runner) ; un binding de notification masqué par le vocabulaire de base se tait avec un avertissement à la place.
|
||||
* **S’enregistrer tardivement.** Une fois que `MCPServer(...)` ou `Client(...)` a renvoyé, l’ensemble des extensions est ce qu’il est.
|
||||
|
||||
Si vous vous battez contre ces murs, vous n’écrivez pas une extension. Vous écrivez un fork. Les murs sont la fonctionnalité : un utilisateur qui lit `extensions=[Apps(), Stamps()]` sait *tout* ce que ces deux-là ont pu toucher.
|
||||
34
i18n/fr/pages/advanced/index.md
Normal file
34
i18n/fr/pages/advanced/index.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ca6988b7503cd2d3]
|
||||
tool: 1
|
||||
---
|
||||
# Avancé {#advanced}
|
||||
|
||||
Tout ce dont un serveur ou un client ordinaire a besoin a sa place, par thème, dans les sections ci-dessus.
|
||||
Cette section regroupe les échappatoires vers lesquelles vous vous tournez quand la couche de commodité
|
||||
de `MCPServer` vous gêne :
|
||||
|
||||
* **[Le Server de bas niveau](low-level-server.md)** : la classe sur laquelle `MCPServer` est construit.
|
||||
Des schémas écrits à la main, des gestionnaires `on_*`, rien de vérifié à votre place, et vos propres
|
||||
méthodes JSON-RPC personnalisées.
|
||||
* **[Pagination](pagination.md)** et **[Middleware](middleware.md)** : deux choses que vous
|
||||
ne pouvez faire *que* sur le `Server` de bas niveau.
|
||||
* **[Extensions](extensions.md)** et **[MCP Apps](apps.md)** : la surface d’extension
|
||||
du protocole. Combinez des paquets d’extension dans un serveur, ou écrivez les vôtres.
|
||||
|
||||
Quelques éléments que vous pourriez légitimement chercher ici se trouvent plutôt là où vous
|
||||
les utilisez réellement :
|
||||
|
||||
* **L’autorisation** se trouve sous **[Exécuter votre serveur](../run/index.md)**, parce que vous
|
||||
protégez un serveur là où vous le déployez.
|
||||
* **OAuth**, **l’assertion d’identité**, la connexion à **plusieurs serveurs** et le
|
||||
**cache** de réponses se trouvent tous sous **[Clients](../client/index.md)**.
|
||||
* **Les requêtes à plusieurs allers-retours** (multi-round-trip) et **les abonnements** se trouvent sous
|
||||
**[Dans votre gestionnaire](../handlers/index.md)**, parce que ce sont deux choses qu’un
|
||||
gestionnaire *fait*.
|
||||
* **Les modèles d’URI** se trouvent sous **[Serveurs](../servers/index.md)**, à côté des ressources.
|
||||
* **[Versions du protocole](../protocol-versions.md)** et
|
||||
**[Fonctionnalités obsolètes](../deprecated.md)** ont chacune leur propre page de premier niveau.
|
||||
|
||||
Si vous n’êtes pas sûr d’avoir besoin de cette section, c’est que vous n’en avez pas besoin.
|
||||
207
i18n/fr/pages/advanced/low-level-server.md
Normal file
207
i18n/fr/pages/advanced/low-level-server.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
---
|
||||
translation:
|
||||
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
|
||||
tool: 1
|
||||
---
|
||||
# Le Server de bas niveau {#the-low-level-server}
|
||||
|
||||
`@mcp.tool()` est une couche. En dessous se trouve une seconde classe de serveur, `Server`, qui parle le MCP brut : vous lui donnez les objets du protocole et elle les place sur la liaison, tels quels.
|
||||
|
||||
`MCPServer` est construit par-dessus. Vous descendez d’un niveau lorsque la couche de confort vous gêne :
|
||||
|
||||
* Vous devez émettre un schéma **exact** (chargé depuis un fichier, généré à partir d’une base de données), et non un schéma dérivé d’une signature Python.
|
||||
* Vous avez besoin d’un contrôle total sur le résultat : `_meta`, `is_error`, chaque clé de `structured_content`.
|
||||
* Vous devez traiter une méthode que MCP ne définit pas.
|
||||
|
||||
Pour tout le reste, restez sur `MCPServer`.
|
||||
|
||||
## Le même outil, à la main {#the-same-tool-by-hand}
|
||||
|
||||
Voici l’outil `search_books` que **[Outils](../servers/tools.md)** écrit en neuf lignes de `@mcp.tool()`, sans le sucre syntaxique :
|
||||
|
||||
```python title="server.py" hl_lines="22 26 32"
|
||||
--8<-- "docs_src/lowlevel/tutorial001.py"
|
||||
```
|
||||
|
||||
Trois choses ont changé, et elles constituent toute l’API de bas niveau :
|
||||
|
||||
* **Les gestionnaires (handlers) sont des paramètres du constructeur.** `on_list_tools=` et `on_call_tool=` vont dans `Server(...)`. Il n’y a pas de décorateurs à ce niveau, et chaque gestionnaire a la même forme : `async (ctx, params) -> result`.
|
||||
* **Vous écrivez le schéma d’entrée.** `Tool.input_schema` est un simple `dict` JSON Schema. Personne ne le dérive d’annotations de type, car il n’y a aucune annotation de type dont le dériver.
|
||||
* **Vous construisez le résultat.** `CallToolResult(content=[TextContent(...)])`, à la main. Rien n’est enveloppé, converti ni déduit d’une annotation de retour.
|
||||
|
||||
`params` est la requête analysée : `CallToolRequestParams` vous donne `.name` et `.arguments`. `ctx` est un `ServerRequestContext` : `ctx.session` pour répondre au client, `ctx.lifespan_context`, `ctx.request_id` et `ctx.meta`, le `_meta` entrant de la requête.
|
||||
|
||||
!!! info
|
||||
Si vous avez utilisé FastAPI, vous connaissez déjà cette relation. `MCPServer` est la couche des décorateurs et des annotations de type ; `Server` est le Starlette qui se trouve en dessous. Ils ne sont pas rivaux : `MCPServer` construit un `Server` et y enregistre des gestionnaires exactement comme ceux-ci.
|
||||
|
||||
### Essayer {#try-it}
|
||||
|
||||
Pas d’Inspector pour celui-ci : `mcp dev` et `mcp run` n’acceptent qu’un `MCPServer`. Le `Client` en mémoire s’en moque ; il accepte un `Server` de bas niveau exactement comme il accepte un `MCPServer` :
|
||||
|
||||
```python title="main.py"
|
||||
import asyncio
|
||||
|
||||
from mcp import Client
|
||||
|
||||
from server import server
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
|
||||
print(result.content)
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```text
|
||||
[TextContent(type='text', text="Found 3 books matching 'dune' (showing up to 5).", annotations=None, meta=None)]
|
||||
```
|
||||
|
||||
Le même texte que celui produit par la version `@mcp.tool()`. Deux différences, en toute honnêteté :
|
||||
|
||||
* `result.structured_content` vaut `None`. Le serveur de haut niveau enveloppe pour vous un `-> str` dans `{"result": ...}` ; ici, personne ne construit ce que vous n’avez pas construit.
|
||||
* `list_tools` renvoie le schéma que **vous** avez saisi, caractère pour caractère. La version de haut niveau avait `"title": "Query"` sur chaque propriété et un `"title": "search_booksArguments"` à la racine : des artefacts de Pydantic. À ce niveau, si quelque chose est sur la liaison, c’est vous qui l’y avez mis.
|
||||
|
||||
## Rien n’est vérifié pour vous {#nothing-is-checked-for-you}
|
||||
|
||||
`MCPServer` rejette un mauvais argument avant même que votre fonction s’exécute, en validant l’appel par rapport au schéma qu’il a généré (**[Outils](../servers/tools.md)**).
|
||||
|
||||
`Server` ne fait pas cela. Votre `input_schema` est *annoncé* au client ; il n’est jamais *appliqué* à `params.arguments`.
|
||||
|
||||
!!! check
|
||||
Appelez `search_books` sans `limit` et votre `args["limit"]` lève `KeyError`. Le client voit :
|
||||
|
||||
```text
|
||||
MCPError: Internal server error
|
||||
```
|
||||
|
||||
Une erreur JSON-RPC, code `-32603`, avec un message volontairement générique : le SDK ne divulgue pas votre traceback à un appelant distant. Le modèle ne découvre jamais ce qu’il a mal fait, il ne peut donc pas réessayer. (Dans un test, `raise_exceptions=True` fait remonter la véritable exception à la place ; voir **[Tests](../get-started/testing.md)**.)
|
||||
|
||||
Cela se généralise. Une exception levée depuis un gestionnaire de bas niveau est **toujours** une erreur de protocole, jamais un résultat d’outil avec `is_error=True`. Si vous voulez que le modèle lise l’échec et se rattrape, validez vous-même `params.arguments` et renvoyez `CallToolResult(content=[TextContent(...)], is_error=True)`. Les deux types d’échec sont le sujet de **[Gérer les erreurs](../servers/handling-errors.md)**.
|
||||
|
||||
## Deux outils, un gestionnaire {#two-tools-one-handler}
|
||||
|
||||
`on_call_tool` est l’unique point d’entrée pour tous les outils du serveur. Vous aiguillez selon `params.name` :
|
||||
|
||||
```python title="server.py" hl_lines="38-43"
|
||||
--8<-- "docs_src/lowlevel/tutorial002.py"
|
||||
```
|
||||
|
||||
* `list_tools` annonce les deux. `call_tool` répartit selon le nom.
|
||||
* La branche `else` compte : `Server` transmettra sans hésiter à votre gestionnaire un `tools/call` pour un nom que vous n’avez jamais listé. Lever une exception à cet endroit transforme l’appel en le même `-32603` que ci-dessus.
|
||||
|
||||
## Sortie structurée, à la main {#structured-output-by-hand}
|
||||
|
||||
Déclarez `output_schema` sur le `Tool` et placez `structured_content` sur le résultat. Les deux vous appartiennent :
|
||||
|
||||
```python title="server.py" hl_lines="19-23 36"
|
||||
--8<-- "docs_src/lowlevel/tutorial003.py"
|
||||
```
|
||||
|
||||
Appelez-le et le résultat porte les deux représentations :
|
||||
|
||||
```json
|
||||
{
|
||||
"content": [{"type": "text", "text": "Found 3 books matching 'dune'."}],
|
||||
"structuredContent": {"matches": 3, "query": "dune"},
|
||||
"isError": false,
|
||||
"resultType": "complete",
|
||||
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}}
|
||||
}
|
||||
```
|
||||
|
||||
Le bloc `_meta` est la marque d’identité du serveur : le SDK l’ajoute à chaque résultat de génération 2026, avec la `version` issue du constructeur (un serveur qui n’en définit aucune renvoie une chaîne vide). Un serveur qui ne doit pas s’identifier peut retirer la clé avec un middleware, lequel est maître des résultats qu’il renvoie.
|
||||
|
||||
Le serveur ne compare jamais les deux champs. Le `Client` de ce SDK, si : renvoyez un `structured_content` qui ne satisfait pas le `output_schema` que vous avez déclaré et `call_tool` lève une `RuntimeError` qui commence par `Invalid structured content returned by tool search_books` puis cite l’échec de `jsonschema`. Promettre un schéma ne coûte rien ; le tenir vous incombe. Toute l’échelle des types de retour et des schémas est dans **[Sortie structurée](../servers/structured-output.md)**.
|
||||
|
||||
## `_meta` : pour l’application, pas pour le modèle {#\_meta-for-the-application-not-the-model}
|
||||
|
||||
`content` est la partie de la réponse que lit le modèle. `structured_content` est la même réponse sous forme de données typées. `_meta` est le troisième canal : des données qui voyagent avec le résultat à destination de l’**application cliente**, sans faire partie de la réponse du tout.
|
||||
|
||||
Utilisez-le pour des identifiants d’enregistrement, des identifiants de trace, tout ce dont votre interface a besoin mais pas votre prompt :
|
||||
|
||||
```python title="server.py" hl_lines="37"
|
||||
--8<-- "docs_src/lowlevel/tutorial004.py"
|
||||
```
|
||||
|
||||
* Vous le construisez sous le nom `_meta=`, le nom sur la liaison. Le client le relit sous la forme `result.meta`.
|
||||
* Préfixez vos clés d’un espace de noms (`bookshop/record_ids`). Les clés `io.modelcontextprotocol/*` sont réservées par le protocole.
|
||||
|
||||
!!! warning
|
||||
`_meta` est une convention entre vous et l’application cliente, pas une garantie sur ce qui parvient
|
||||
au modèle. L’hôte décide de ce qu’il affiche. Ne mettez jamais de secret dans quelque partie que ce soit d’un résultat d’outil.
|
||||
|
||||
## Les capacités suivent vos gestionnaires {#capabilities-follow-your-handlers}
|
||||
|
||||
Un `Server` annonce exactement les familles de méthodes pour lesquelles vous lui avez fourni des gestionnaires. Le `Bookshop` ci-dessus passe `on_list_tools` et `on_call_tool` et rien d’autre, donc un client qui s’y connecte voit :
|
||||
|
||||
```json
|
||||
{"tools": {"listChanged": false}}
|
||||
```
|
||||
|
||||
Pas de `resources`, pas de `prompts` : rien ne les soutient. Passez `on_list_prompts` et `prompts` apparaît ; passez `on_completion` et `completions` apparaît.
|
||||
|
||||
`MCPServer` annonce toujours les outils, les ressources et les prompts, que vous en ayez enregistré ou non, car ses managers existent toujours. À ce niveau, la déclaration *est* l’appel au constructeur.
|
||||
|
||||
## Le type générique du cycle de vie {#the-lifespan-generic}
|
||||
|
||||
`Server` est générique sur le type que produit son cycle de vie (lifespan). Annotez-le une fois et l’objet est typé partout où il apparaît :
|
||||
|
||||
```python title="server.py" hl_lines="24-26 44-45 50"
|
||||
--8<-- "docs_src/lowlevel/tutorial005.py"
|
||||
```
|
||||
|
||||
* Le cycle de vie est un `Callable[[Server[Catalog]], AbstractAsyncContextManager[Catalog]]` ; `@asynccontextmanager` sur un générateur `async` vous donne exactement cela.
|
||||
* Ce qu’il produit via `yield` devient `ctx.lifespan_context`, et comme les gestionnaires sont annotés `ServerRequestContext[Catalog]`, `.search(...)` bénéficie de l’autocomplétion et de la vérification de types.
|
||||
* On y entre une fois au démarrage du serveur et on en sort une fois à son arrêt. Le démarrage, l’arrêt et la version `MCPServer` de la même idée sont dans **[Cycle de vie](../handlers/lifespan.md)**.
|
||||
|
||||
Sans `lifespan=`, `ctx.lifespan_context` est un `dict` vide.
|
||||
|
||||
## Une méthode à vous {#a-method-of-your-own}
|
||||
|
||||
Le constructeur couvre les méthodes que MCP définit. `add_request_handler` couvre tout le reste :
|
||||
|
||||
```python title="server.py" hl_lines="35-36 39-40 43-44 48"
|
||||
--8<-- "docs_src/lowlevel/tutorial006.py"
|
||||
```
|
||||
|
||||
* Le premier argument est la chaîne de la méthode. Les notifications ont un jumeau, `add_notification_handler`.
|
||||
* `params_type` est le modèle par rapport auquel les `params` entrants sont validés **avant** l’exécution de votre gestionnaire ; les méthodes personnalisées *ont* donc droit à la validation dont les outils sont privés. Dérivez de `RequestParams` pour que le champ `_meta` s’analyse comme celui de toute autre méthode.
|
||||
* Le gestionnaire renvoie un `BaseModel`, un `dict` ou `None`. Le SDK le sérialise dans le résultat JSON-RPC.
|
||||
|
||||
Une réserve, en toute honnêteté : le `Client` de haut niveau n’a de verbes que pour les méthodes que MCP définit, il n’y a donc pas de `client.reindex()`. Une méthode propriétaire s’adresse à un pair qui sait déjà qu’elle existe : un client que vous livrez aussi, ou un autre de vos services parlant JSON-RPC.
|
||||
|
||||
Une méthode que vous ne pouvez pas vous approprier :
|
||||
|
||||
```text
|
||||
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
|
||||
use Server.middleware to observe or wrap initialization
|
||||
```
|
||||
|
||||
La poignée de main (handshake) appartient à l’exécuteur (runner). Vous êtes libre de remplacer `server/discover`, `ping` et toutes les autres méthodes intégrées.
|
||||
|
||||
!!! tip
|
||||
`Server.middleware`, mentionné dans cette erreur, enveloppe **chaque** message entrant, `initialize` compris. Si ce que vous voulez est observer ou réécrire le trafic plutôt que répondre à une nouvelle méthode, commencez par **[Middleware](middleware.md)**.
|
||||
|
||||
## Les autres gestionnaires {#the-other-handlers}
|
||||
|
||||
Chacun d’eux correspond à une idée pour laquelle vous avez désormais le vocabulaire ; chacun a sa propre page.
|
||||
|
||||
* `on_call_tool`, `on_get_prompt` et `on_read_resource` peuvent renvoyer un `InputRequiredResult` au lieu de leur résultat normal pour mettre l’appel en pause et demander une saisie au client ; voir **[Requêtes à plusieurs allers-retours (multi-round-trip)](../handlers/multi-round-trip.md)**. Fidèle à ce niveau, rien n’est installé pour vous : là où `MCPServer` scelle `requestState` par défaut, ici le `request_state` que vous définissez traverse la liaison exactement tel qu’écrit, jusqu’à ce que vous optiez pour `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))` : une seule ligne (les deux noms s’importent depuis `mcp.server.request_state`) pour un scellement et une vérification identiques à ceux qu’effectue `MCPServer` (**[Protéger `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**).
|
||||
* `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` ont la même forme `(ctx, params) -> result` pour les autres primitives.
|
||||
* `on_subscriptions_listen` sert le flux `subscriptions/listen` de la version 2026-07-28. Passez un `ListenHandler` construit sur un `SubscriptionBus` et publiez des événements sur le bus depuis vos autres gestionnaires ; voir **[Abonnements](../handlers/subscriptions.md)** pour la composition complète.
|
||||
* `server.streamable_http_app()` renvoie la même application Starlette que celle de `MCPServer` ; déployez-la comme **[Exécuter votre serveur](../run/index.md)** déploie n’importe quelle autre application ASGI. Il n’y a pas de `server.run(transport=...)` à ce niveau : `server.run(read_stream, write_stream, server.create_initialization_options())` pilote une connexion sur une paire de flux, et cette seule ligne dit tout.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* Le `Server` de bas niveau reçoit ses gestionnaires sous forme de **paramètres de constructeur** `on_*` ; chaque gestionnaire est `async (ctx, params) -> result`.
|
||||
* Vous écrivez le dict `input_schema` et vous construisez le `CallToolResult`. Rien n’est dérivé, enveloppé ni validé pour vous.
|
||||
* Une exception dans un gestionnaire est une erreur de protocole `-32603`. Une erreur d’outil que le modèle peut lire est un `CallToolResult` avec `is_error=True` que **vous** renvoyez.
|
||||
* Le `_meta` du résultat s’adresse à l’application cliente, pas au modèle.
|
||||
* `Server[T]` est générique sur ce que produit son cycle de vie ; `ctx.lifespan_context` est un `T` typé.
|
||||
* `add_request_handler(method, params_type, handler)` sert n’importe quelle méthode. `initialize` est réservée.
|
||||
* Les capacités qu’annonce un `Server` découlent des gestionnaires que vous avez enregistrés.
|
||||
|
||||
`Client(server)` a traité les deux serveurs de façon identique parce qu’ils *sont* le même protocole, et c’est tout l’intérêt. La couche suivante vers le bas n’est pas une classe du tout : c’est le **[Middleware](middleware.md)**.
|
||||
132
i18n/fr/pages/advanced/middleware.md
Normal file
132
i18n/fr/pages/advanced/middleware.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
---
|
||||
translation:
|
||||
sections: [6048b4f308edbb8c, 068bda0f21ee9c1b, c3e565b61acd75c5, c62422b159c6ed09, 47204fab253cc45c]
|
||||
tool: 1
|
||||
---
|
||||
# Middleware {#middleware}
|
||||
|
||||
Un **middleware** est une fonction asynchrone qui enveloppe chaque message que votre serveur reçoit.
|
||||
|
||||
Vous l’écrivez sous la forme `async (ctx, call_next)` et vous l’ajoutez à `server.middleware`. C’est toute l’API.
|
||||
|
||||
!!! warning
|
||||
La liste de middlewares est marquée **provisoire** dans le code source : sa signature et sa
|
||||
sémantique peuvent changer dans une version mineure 2.x. Utilisez-la pour *observer*
|
||||
(chronométrage, journalisation, traçage) et pour *refuser* des messages ; n’en faites pas la
|
||||
fondation sur laquelle repose votre serveur.
|
||||
|
||||
`MCPServer` reçoit la liste à la construction (`MCPServer(name, middleware=[...])`) et l’expose sous
|
||||
`mcp.middleware` ; le `Server` bas niveau expose la même liste sous `server.middleware`. L’exemple
|
||||
ci-dessous utilise le `Server` bas niveau ; si `Server(name, on_call_tool=...)` est nouveau pour
|
||||
vous, lisez d’abord **[Le Server bas niveau](low-level-server.md)**.
|
||||
|
||||
## Un middleware de chronométrage {#a-timing-middleware}
|
||||
|
||||
Un serveur, un outil, un middleware qui journalise le temps pris par chaque message :
|
||||
|
||||
```python title="server.py" hl_lines="39-45 49"
|
||||
--8<-- "docs_src/middleware/tutorial001.py"
|
||||
```
|
||||
|
||||
* `ctx` est le même `ServerRequestContext` que celui que reçoivent vos gestionnaires (handlers).
|
||||
`ctx.method` est la chaîne de méthode brute ; `ctx.params` contient les paramètres bruts,
|
||||
**avant** toute validation.
|
||||
* `call_next(ctx)` exécute le reste de la chaîne : la validation, la recherche du gestionnaire,
|
||||
votre gestionnaire. Renvoyez ce qu’il a renvoyé et la réponse reste intacte.
|
||||
* Le `try`/`finally` est délibéré : un gestionnaire qui lève une exception est tout de même
|
||||
chronométré, car l’échec atteint votre middleware sous la forme de l’exception qui sort de
|
||||
`call_next`.
|
||||
* `server.middleware.append(...)` l’enregistre. La liste s’exécute de l’extérieur vers
|
||||
l’intérieur, donc `middleware[0]` est celui qui est le plus proche de la liaison.
|
||||
|
||||
### Essayer {#try-it}
|
||||
|
||||
Connectez un client, listez les outils, appelez-en un. Votre journal contient **trois** lignes :
|
||||
|
||||
```text
|
||||
server/discover took 18.3 ms
|
||||
tools/list took 0.1 ms
|
||||
tools/call took 0.1 ms
|
||||
```
|
||||
|
||||
Vous avez fait deux appels et obtenu trois lignes. La première est `server/discover` : la requête
|
||||
que le client a envoyée pour établir la connexion, avant que vous ne demandiez quoi que ce soit.
|
||||
|
||||
C’est tout l’intérêt. Le middleware enveloppe **chaque** message entrant :
|
||||
|
||||
* La mise en place de la connexion : `server/discover`, ou `initialize` et
|
||||
`notifications/initialized` sur une session historique.
|
||||
* Chaque requête et chaque notification. Pour une notification, `ctx.request_id is None`,
|
||||
`call_next(ctx)` renvoie `None`, et tout ce que vous renvoyez est ignoré.
|
||||
* Même une méthode pour laquelle le serveur n’a pas de gestionnaire : `call_next` lève
|
||||
`MCPError(-32601, "Method not found")` *à travers* votre middleware en route vers le client.
|
||||
|
||||
## Ce que vous pouvez y faire {#what-you-can-do-inside-one}
|
||||
|
||||
Du geste le plus anodin à celui devant lequel vous devriez le plus hésiter :
|
||||
|
||||
* **Observer.** Chronométrer, compter, journaliser. C’est l’exemple ci-dessus.
|
||||
* **Refuser.** Levez une `MCPError` *au lieu* d’appeler `call_next(ctx)` et ce message-là
|
||||
reçoit pour réponse une erreur JSON-RPC. La connexion reste ouverte ; le message suivant passe.
|
||||
C’est ainsi qu’un serveur contrôle l’accès à `subscriptions/listen` appelant par appelant : la
|
||||
section **[Décider qui peut observer](../handlers/subscriptions.md#deciding-who-may-watch)** de
|
||||
la page Abonnements détaille la démarche.
|
||||
* **Réécrire.** `ctx` est une dataclass : `await call_next(dataclasses.replace(ctx, params=...))`
|
||||
transmet au reste de la chaîne d’autres paramètres que ceux envoyés par le client. Ne faites
|
||||
jamais cela pour `initialize` : le résultat que le client reçoit en retour est construit à
|
||||
partir de vos paramètres réécrits, mais le serveur fixe l’état de sa connexion à partir des
|
||||
paramètres d’origine reçus sur la liaison. Les deux côtés peuvent terminer la poignée de main
|
||||
(handshake) en désaccord sur ce qu’ils ont négocié.
|
||||
* **Répondre.** Renvoyez un résultat sans appeler `call_next(ctx)` et il part au client comme
|
||||
votre réponse. `call_next` vous remet la forme finale telle qu’elle circule sur la liaison, et
|
||||
le pipeline ne retouche jamais ce que vous renvoyez ; toute l’enveloppe est donc à votre
|
||||
charge : sur une connexion de génération 2026, cela inclut l’estampille `_meta` `serverInfo`,
|
||||
que le SDK ajoute aux résultats des gestionnaires mais pas aux vôtres.
|
||||
|
||||
!!! check
|
||||
`initialize` fait partie de ce que le middleware enveloppe, et c’est le *seul* hook dont
|
||||
vous disposez pour lui. Essayez d’en prendre le contrôle avec `add_request_handler` et le
|
||||
SDK refuse :
|
||||
|
||||
```text
|
||||
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
|
||||
use Server.middleware to observe or wrap initialization
|
||||
```
|
||||
|
||||
!!! warning
|
||||
`initialize` est traité en ligne : le serveur ne lit aucun autre message entrant tant que
|
||||
votre chaîne de middlewares n’est pas revenue. Attendre une requête du serveur vers le client
|
||||
(`ctx.session.send_request(...)`, une élicitation (elicitation)) pendant le traitement de
|
||||
`initialize` **provoque donc l’interblocage de la connexion** : la réponse que vous attendez
|
||||
ne pourra jamais être lue. Les notifications envoyées sans attente de réponse ne posent pas
|
||||
de problème.
|
||||
|
||||
## Le seul middleware activé par défaut {#the-one-middleware-that-ships-on-by-default}
|
||||
|
||||
Le SDK fournit exactement un middleware, et il figure déjà dans la liste de votre serveur : celui
|
||||
qui émet un span OpenTelemetry pour chaque message. Vous ne l’ajoutez pas et, la plupart du temps,
|
||||
vous n’y pensez pas. Il ne fait rien tant que vous n’installez pas d’exporteur, et il a sa propre
|
||||
page : **[OpenTelemetry](../run/opentelemetry.md)**.
|
||||
|
||||
!!! info
|
||||
Si vous avez déjà écrit un middleware ASGI, vous connaissez cette forme. Le
|
||||
`(scope, receive, send)` de Starlette est devenu `(ctx, call_next)`, et il s’exécute *après*
|
||||
le transport, sur le message décodé plutôt que sur la requête HTTP brute. Les deux se
|
||||
composent : un middleware Starlette sur `streamable_http_app()` voit du HTTP ; celui-ci voit
|
||||
du MCP.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* Un middleware est `async (ctx, call_next) -> result`, passé via `MCPServer(middleware=[...])`
|
||||
(ou ajouté à `mcp.middleware`), et ajouté à `server.middleware` sur le `Server` bas niveau.
|
||||
* Il enveloppe **chaque** message entrant (`server/discover`, `initialize`, requêtes,
|
||||
notifications, méthodes inconnues) et s’exécute de l’extérieur vers l’intérieur.
|
||||
* `ctx.request_id is None` est ce qui distingue une notification d’une requête.
|
||||
* Levez une exception au lieu d’appeler `call_next` pour refuser un message ; la connexion
|
||||
survit.
|
||||
* Le traçage OpenTelemetry du SDK est lui aussi un middleware, déjà dans la liste. Voir
|
||||
**[OpenTelemetry](../run/opentelemetry.md)**.
|
||||
* Toute cette surface est provisoire. Servez-vous-en pour observer ; ne construisez pas dessus.
|
||||
|
||||
C’est tout ce qui enveloppe une requête. Quant à savoir si la requête a seulement le droit de
|
||||
s’exécuter, c’est l’**[Autorisation](../run/authorization.md)** qui en décide.
|
||||
85
i18n/fr/pages/advanced/pagination.md
Normal file
85
i18n/fr/pages/advanced/pagination.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
|
||||
tool: 1
|
||||
---
|
||||
# Pagination {#pagination}
|
||||
|
||||
La plupart des serveurs n’en ont jamais besoin.
|
||||
|
||||
`MCPServer` répond à chaque requête `list_*` avec tout ce qu’il a, en une seule page, `next_cursor=None`. Pour quelques dizaines d’outils, de ressources ou de prompts, c’est la bonne réponse et il n’y a rien à configurer.
|
||||
|
||||
La pagination sert au serveur dont la liste de ressources est en réalité une base de données : des milliers de lignes qu’il refuse de sérialiser en une seule réponse. La réponse du protocole est un **curseur** : le serveur renvoie une page accompagnée d’un jeton opaque, et le client renvoie ce jeton pour obtenir la page suivante.
|
||||
|
||||
`@mcp.resource()` n’offre aucun point d’accroche pour cela. Pour paginer, vous écrivez vous-même le gestionnaire (handler) de liste, sur le **[Server de bas niveau](low-level-server.md)**.
|
||||
|
||||
## Un serveur qui pagine {#a-server-that-pages}
|
||||
|
||||
```python title="server.py" hl_lines="12 15-16"
|
||||
--8<-- "docs_src/pagination/tutorial001.py"
|
||||
```
|
||||
|
||||
* Sur un `Server` de bas niveau, les gestionnaires sont des arguments du constructeur, pas des décorateurs. `on_list_resources` répond à chaque requête `resources/list` ; c’est tout le branchement nécessaire.
|
||||
* Chaque gestionnaire paginé est typé `params: PaginatedRequestParams | None`, et l’exemple accepte les deux. Sur une connexion, cependant, le SDK ne vous passe jamais `None` (une requête sans membre `params` arrive au gestionnaire sous la forme du modèle avec ses valeurs par défaut), donc le signal qui compte est `params.cursor is None` : **commencer par le début**.
|
||||
* C’est vous qui décidez ce qu’*est* un curseur. Ici, c’est un décalage (offset) rendu sous forme de chaîne. Un horodatage, une clé primaire, un blob base64 : tout ce que vous pouvez émettre à l’aller et reconnaître au retour.
|
||||
* `next_cursor=None` est votre façon de dire « c’était la dernière page ». Il n’y a ni décompte, ni total, ni `has_more`. `None` est le signal à lui seul.
|
||||
|
||||
!!! tip
|
||||
Une valeur de `PAGE_SIZE` de 10 rend l’exemple lisible. Choisissez la vôtre par point de terminaison : une liste de
|
||||
ressources d’une ligne peut se permettre une page de 500 ; une liste de gros modèles de prompts, non.
|
||||
Le client n’a pas son mot à dire, et c’est voulu.
|
||||
|
||||
### Essayer {#try-it}
|
||||
|
||||
`Client(server)` se connecte à un `Server` de bas niveau en mémoire exactement comme il se connecte à un `MCPServer`.
|
||||
|
||||
Appelez `list_resources()` sans argument. Vous obtenez dix ressources, de `book-1` à `book-10`, et `next_cursor` vaut la chaîne `"10"`.
|
||||
|
||||
Renvoyez-la avec `list_resources(cursor="10")` : la première ressource est `book-11`, le nouveau `next_cursor` vaut `"20"`.
|
||||
|
||||
La dixième page revient avec `next_cursor` à `None`. Terminé.
|
||||
|
||||
## La boucle côté client {#the-client-loop}
|
||||
|
||||
Chaque méthode `list_*` de `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) accepte un argument nommé `cursor=`. Vider une liste paginée tient en un `while True` :
|
||||
|
||||
```python title="client.py" hl_lines="26-32"
|
||||
--8<-- "docs_src/pagination/tutorial002.py"
|
||||
```
|
||||
|
||||
* `cursor` démarre à `None`, donc la première requête ne porte aucun curseur.
|
||||
* Étendez la liste **avant** de regarder `next_cursor` : la dernière page contient elle aussi des ressources.
|
||||
* `next_cursor is None` est la sortie. Toute autre valeur repart directement dans `cursor=`, telle quelle.
|
||||
|
||||
Lancez son `main()` et il affiche `100 resources` : dix pages de dix, assemblées par une boucle qui n’a jamais su qu’il y avait dix pages.
|
||||
|
||||
C’est la même boucle que montre **[Le client](../client/index.md)** pour chaque verbe `list_*`, et elle ne coûte rien face à un serveur qui ne pagine pas : `next_cursor` vaut `None` dès la première réponse et la boucle s’exécute une seule fois.
|
||||
|
||||
## Les trois règles {#the-three-rules}
|
||||
|
||||
**Les curseurs sont opaques.** Un client ne doit jamais en analyser, en construire ni en deviner un. La seule source légitime d’un curseur est le `next_cursor` de la page précédente, tel quel.
|
||||
|
||||
**Le serveur choisit la taille de page.** Il n’y a pas de `limit=` dans le protocole. S’il vous faut une autre taille de page, vous modifiez le serveur.
|
||||
|
||||
**Un client qui ignore la pagination fonctionne quand même.** Il appelle `list_resources()` une fois, obtient les dix premières, et ne remarque jamais le `next_cursor` qu’il a jeté. Rien ne casse ; il en voit moins.
|
||||
|
||||
!!! check
|
||||
Opaque veut dire opaque. Inventez un curseur (`list_resources(cursor="page-2")`) et le
|
||||
protocole ne peut rien pour vous. Ce serveur tente `int("page-2")`, le gestionnaire lève une exception,
|
||||
et ce qui revient au client est :
|
||||
|
||||
```text
|
||||
MCPError(-32603, 'Internal server error', None)
|
||||
```
|
||||
|
||||
Un curseur que vous n’avez pas obtenu du serveur est un bogue, pas une demande de fonctionnalité.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* `MCPServer` renvoie tout en une seule page. La pagination est facultative, et vous l’activez sur le `Server` de bas niveau.
|
||||
* `on_list_resources` (ainsi que `on_list_tools`, `on_list_prompts`, `on_list_resource_templates`) reçoit `PaginatedRequestParams | None` ; `params.cursor` vaut `None` pour la première page.
|
||||
* Vous renvoyez une page plus un `next_cursor` : n’importe quelle chaîne que vous reconnaîtrez plus tard, ou `None` quand il ne reste rien.
|
||||
* La boucle côté client : passez `cursor=`, accumulez, répétez jusqu’à ce que `next_cursor is None`.
|
||||
* Les curseurs sont opaques, la taille de page appartient au serveur, et un client qui ne pagine pas obtient quand même la première page.
|
||||
|
||||
Le reste de l’API `Server` écrite à la main (`on_call_tool`, les dicts `input_schema`, `_meta`) se trouve dans **[Le Server de bas niveau](low-level-server.md)**.
|
||||
122
i18n/fr/pages/client/caching.md
Normal file
122
i18n/fr/pages/client/caching.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
translation:
|
||||
sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800]
|
||||
tool: 1
|
||||
---
|
||||
# Indications de mise en cache {#caching-hints}
|
||||
|
||||
Sur le protocole 2026-07-28, chaque résultat qu’un serveur renvoie pour `tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, `resources/read` et `server/discover` porte deux champs : `ttlMs`, le nombre de millisecondes pendant lesquelles un client peut considérer le résultat comme frais, et `cacheScope`, qui indique si un résultat mis en cache peut être partagé entre utilisateurs (`"public"`) ou appartient à un seul contexte d’autorisation (`"private"`).
|
||||
|
||||
Le serveur ne met rien en cache. Ces champs sont une *déclaration* : « cette liste d’outils est la même pour tout le monde et ne changera pas pendant une minute ». Un client (ou une passerelle placée devant vous) peut alors s’épargner l’aller-retour. Respecter ces indications relève du choix du client ; les émettre est le travail du serveur, et le SDK le fait pour vous.
|
||||
|
||||
Par défaut, chaque résultat indique `ttlMs: 0, cacheScope: "private"` : périmé immédiatement, jamais partagé. C’est toujours sûr et toujours conforme. Si vos listes sont réellement stables et identiques pour tous les appelants, dites-le à la construction :
|
||||
|
||||
```python title="server.py" hl_lines="5-8"
|
||||
--8<-- "docs_src/caching/tutorial001.py"
|
||||
```
|
||||
|
||||
* Le dictionnaire est indexé par **nom de méthode**, et les six méthodes pouvant être mises en cache sont les seules clés autorisées. Le paramètre est typé `Mapping[CacheableMethod, CacheHint]` : votre éditeur complète donc les clés automatiquement et signale une faute de frappe avant l’exécution ; tout ce qui échappe au vérificateur de types lève une exception à la construction.
|
||||
* Une méthode que vous ne mentionnez pas garde les valeurs par défaut. Le dictionnaire est un ensemble de surcharges, pas un manifeste.
|
||||
* `CacheHint(ttl_ms=5_000)` n’a pas défini `scope`, qui reste donc `"private"` : cinq secondes de fraîcheur, par appelant. La portée et le TTL sont deux décisions indépendantes.
|
||||
* `"server/discover"` est aussi une clé autorisée, puisque le résultat de découverte peut être mis en cache comme n’importe quelle liste.
|
||||
|
||||
!!! warning
|
||||
`cacheScope: "public"` signifie que *n’importe qui* peut recevoir votre réponse mise en cache. Une passerelle
|
||||
partagée transmettra sans hésiter le résultat d’un utilisateur à un autre, même lorsque la requête était
|
||||
authentifiée. Ne marquez un résultat `"public"` que s’il est identique pour chaque appelant, et
|
||||
n’utilisez jamais `cacheScope` comme contrôle d’accès : c’est une étiquette, pas un verrou.
|
||||
|
||||
## Surcharge par gestionnaire {#per-handler-override}
|
||||
|
||||
Sur le `Server` bas niveau, les gestionnaires (handlers) construisent leurs résultats à la main, et `ttl_ms` / `cache_scope` sont de simples champs des modèles de résultat. Un gestionnaire qui les définit explicitement l’emporte toujours sur le dictionnaire du constructeur, champ par champ :
|
||||
|
||||
```python title="server.py" hl_lines="10 16"
|
||||
--8<-- "docs_src/caching/tutorial002.py"
|
||||
```
|
||||
|
||||
Le gestionnaire a indiqué `ttl_ms=1_000` et rien sur la portée. Sur la liaison : `ttlMs: 1000` (la valeur du gestionnaire, pas le `60_000` du dictionnaire) et `cacheScope: "public"` (la valeur du dictionnaire, puisque le gestionnaire ne l’a pas définie). L’explicite l’emporte sur le configuré, et le configuré sur la valeur par défaut. Cela vaut champ par champ : un gestionnaire peut donc fixer un champ et laisser l’autre à la politique du serveur.
|
||||
|
||||
C’est aussi l’échappatoire pour les comportements dynamiques que le constructeur ne peut pas connaître : un gestionnaire qui filtre `resources/read` par utilisateur peut renvoyer `cache_scope="private"` pour un URI donné sur un serveur par ailleurs public.
|
||||
|
||||
Une réserve sur les listes paginées : le protocole exige **le même `cacheScope` sur chaque page** d’une même liste. Le dictionnaire du constructeur y satisfait par construction, puisqu’il est indexé par méthode et non par page. Mais un gestionnaire qui surcharge lui-même la portée devient responsable de cette cohérence : surchargez-la sur *chaque* page, jamais uniquement lorsqu’un curseur est présent, sinon la page un et la page deux se contrediront.
|
||||
|
||||
## Ce que voit le client {#what-the-client-sees}
|
||||
|
||||
Sur une session 2026-07-28, `Client` respecte les indications pour vous : il embarque un cache de réponses, activé par défaut. Un résultat qui arrive avec un `ttlMs` est stocké, et un appel identique effectué dans ce TTL est servi depuis le cache, sans aller-retour. Un résultat qui ne porte *aucune* indication n’est pas mis en cache : les résultats sans indication reçoivent `CacheConfig.default_ttl_ms`, dont la valeur par défaut est `0` (périmé immédiatement), si bien qu’un serveur qui ne déclare rien voit exactement le même trafic, appel pour appel, qu’auparavant.
|
||||
|
||||
```python title="client.py" hl_lines="33 35 38"
|
||||
--8<-- "docs_src/caching/tutorial003.py"
|
||||
```
|
||||
|
||||
Quatre appels, trois récupérations. Le deuxième appel a trouvé une entrée fraîche et n’a jamais atteint le serveur ; avancer l’horloge (injectée) au-delà du TTL a fait que le troisième récupère à nouveau ; le quatrième a indiqué `cache_mode="refresh"`. Cet argument nommé existe sur les cinq verbes avec cache (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`) :
|
||||
|
||||
* `"use"` (la valeur par défaut) sert une entrée fraîche s’il y en a une, et stocke le résultat récupéré sinon.
|
||||
* `"refresh"` ne sert jamais depuis le cache : il récupère et stocke le résultat, en remplaçant ce qui était en cache.
|
||||
* `"bypass"` effectue l’aller-retour sans toucher du tout au cache : ni lecture, ni écriture.
|
||||
|
||||
Une règle prime sur `"use"` : **les appels portant `meta` atteignent toujours le serveur.** Une requête avec `meta` défini (un jeton de progression, des champs de traçage) attend une requête sur la liaison ; sous `cache_mode="use"`, elle est donc traitée comme `"refresh"` : la lecture du cache est sautée, et le résultat récupéré remplace quand même l’entrée en cache. `"bypass"` et un `"refresh"` explicite se comportent comme d’habitude.
|
||||
|
||||
Pour désactiver entièrement la mise en cache, construisez avec `Client(server, cache=None)` : chaque appel redevient un aller-retour, et `cache_mode`, bien que toujours accepté, n’a aucun effet.
|
||||
|
||||
La portée est elle aussi respectée automatiquement : les entrées `"private"` sont indexées sur la *partition* du cache (ci-dessous), tandis que les entrées `"public"` peuvent opter pour un partage plus large. Et **les notifications priment sur le TTL** pour les entrées exactes qu’elles désignent : une notification `list_changed` évince la liste correspondante en cache, et `resources/updated` évince la lecture en cache stockée exactement sous son URI, aussi fraîches soient-elles. Sur une connexion 2026-07-28, ces notifications arrivent sur un flux `subscriptions/listen` que vous ouvrez avec `client.listen(...)`, et l’éviction se termine avant que votre observateur ne voie l’événement ; tous les détails sont dans **[Abonnements](subscriptions.md)**.
|
||||
|
||||
Une réserve sur `resources/updated` : l’éviction ne porte que sur l’URI exact. Le contrat du magasin n’a ni opération d’énumération ni de parcours (comme l’implémentation TypeScript de référence), donc une notification portant l’URI d’une *sous*-ressource n’évince pas la lecture en cache de son parent. Si votre serveur signale ainsi des sous-ressources, récupérez à nouveau le parent avec `cache_mode="refresh"`.
|
||||
|
||||
### Configurer le cache : `CacheConfig` {#configuring-it-cacheconfig}
|
||||
|
||||
```python
|
||||
from mcp.client import CacheConfig
|
||||
|
||||
client = Client("https://api.example.com/mcp", cache=CacheConfig(default_ttl_ms=5_000))
|
||||
```
|
||||
|
||||
* `store` : l’endroit où vivent les entrées. Par défaut, un nouveau magasin en mémoire par client ; passez votre propre implémentation de `ResponseCacheStore` (adossée à Redis, par exemple) pour partager un cache entre clients ou processus. Les types du contrat (`ResponseCacheStore`, `CacheKey`, `CacheEntry` et le `InMemoryResponseCacheStore` par défaut) sont importables depuis `mcp.client`. Une recherche peut émettre jusqu’à deux `get` séquentiels sur le magasin (la branche privée, puis la publique) ; dimensionnez donc en conséquence vos attentes de latence pour un magasin distant. Un magasin personnalisé **exige** une `partition` explicite.
|
||||
* `partition` : l’étiquette de contexte d’autorisation qui empêche les entrées `"private"` d’un principal d’être servies à un autre au sein d’un magasin partagé.
|
||||
* `target_id` : identité explicite du serveur, pour les transports personnalisés et les serveurs en processus (ci-dessous).
|
||||
* `default_ttl_ms` : TTL appliqué aux résultats qui ne portent aucune indication `ttlMs`. La valeur par défaut `0` laisse les résultats sans indication hors du cache.
|
||||
* `share_public` : servir entre partitions les entrées que le serveur affirme `"public"` (ci-dessous). Désactivé par défaut.
|
||||
* `clock` : la source d’horloge murale, en secondes depuis l’epoch. Injectez-en une, comme le fait l’exemple ci-dessus, et les tests d’expiration n’ont pas besoin d’attendre.
|
||||
|
||||
!!! warning "Partition = principal vérifié"
|
||||
Dérivez `partition` d’**informations d’identification vérifiées**, comme le sujet d’un jeton validé. Ne la dérivez jamais de données fournies par la requête, ni de l’URL du serveur (l’identité du serveur est un axe de clé distinct). Le SDK est une bibliothèque sans authentification propre : l’ancre de confiance est celui qui construit le `CacheConfig`, c’est-à-dire le déploiement, pas le locataire. Une passerelle multi-locataire crée un `CacheConfig` par principal authentifié.
|
||||
|
||||
La partition est aussi figée pour toute la durée de vie du `Client`. Si le contexte d’autorisation de la connexion change en cours de session (une réauthentification sous un autre principal, par exemple), le cache ne suit pas ; construisez un nouveau `Client` pour le nouveau principal.
|
||||
|
||||
Les clés du cache portent aussi **l’identité du serveur** : la chaîne d’URL que vous avez appelée, débarrassée de toute partie userinfo `user:pass@` et sinon conservée à l’octet près. Pas de normalisation de la casse, pas de réordonnancement des paramètres de requête, pas de nettoyage de la barre oblique finale. Sous-normaliser ne coûte que du partage, alors que sur-normaliser pourrait fusionner deux locataires (`?tenant=a` et `?tenant=b`) : des URL superficiellement différentes ne partagent tout simplement pas d’entrées. Lorsqu’il n’y a pas d’URL (un serveur en processus, ou une instance de `Transport`), le client reçoit à la place une identité aléatoire par instance ; définissez `CacheConfig.target_id` pour nommer le serveur (avec un magasin personnalisé, c’est obligatoire, et la construction vous le dit). L’identité est hachée en sha256 avant d’entrer dans le matériau de clé, si bien qu’une URL portant des secrets dans sa chaîne de requête n’apparaît jamais dans les clés du magasin. Ne journalisez pas non plus vous-même la forme avant hachage.
|
||||
|
||||
!!! warning "`share_public` fait confiance au serveur, pour tout le parc"
|
||||
Par défaut, même les entrées `"public"` restent dans leur partition. `share_public=True` sert les entrées que le serveur a marquées `cacheScope: "public"` à **toutes** les partitions qui utilisent le magasin, en faisant confiance à la classification du serveur au nom de chacune d’elles. Un serveur qui appose `"public"` sur des données propres à un locataire (par bogue ou par malveillance) fait alors fuiter la réponse d’un locataire vers les autres. L’option est délibérément limitée au constructeur : le `cache_mode` par appel peut restreindre la mise en cache, mais rien au niveau de l’appel ne peut élargir le partage.
|
||||
|
||||
### Ce que le cache ne fait jamais {#what-the-cache-never-does}
|
||||
|
||||
* **Les appels au niveau session le contournent.** `client.session.list_tools()` et consorts font toujours l’aller-retour ; le cache vit sur les verbes de `Client`.
|
||||
* **`server/discover` reste en dehors.** Le résultat de découverte est livré une fois, à la connexion, et n’entre jamais dans le cache de réponses, même lorsqu’il porte un `ttlMs`. Si vous en persistez un vous-même pour éviter la sonde de reconnexion ([`prior_discover`](../protocol-versions.md#reconnecting-with-prior_discover)), sa fraîcheur relève de votre propre suivi : `DiscoverResult` porte `ttl_ms` et `cache_scope`, déjà analysés, précisément à cette fin.
|
||||
* **Les pages de continuation ne sont jamais mises en cache.** Seuls les appels sans curseur participent. Une page de continuation rejetée pour curseur expiré *évince* bien la liste en cache, car la liste a changé entre-temps.
|
||||
* **Les lectures à plusieurs allers-retours (multi-round-trip) ne sont jamais mises en cache.** Un `read_resource` amorcé avec `input_responses`/`request_state`, ou qui se résout au fil de tours de saisie, n’entre jamais dans le cache (un MUST de la spécification).
|
||||
* **L’éviction par notification a besoin de notifications.** L’éviction ne vaut que ce que vaut la livraison du transport, et le chemin moderne en processus (`Client(server)` avec le `mode="auto"` par défaut) ne livre pas aujourd’hui les notifications autonomes.
|
||||
* **L’éviction se produit à terme, pas instantanément.** Les notifications qui arrivent par la liaison sont distribuées depuis des tâches lancées à part ; un appel en concurrence avec l’arrivée d’une notification peut donc se voir servir une fois de plus l’entrée d’avant l’éviction ; la fenêtre est bornée par la latence de distribution, et l’éviction a tout de même lieu.
|
||||
* **Pas de stale-if-error.** Une entrée expirée n’est jamais servie parce que la nouvelle récupération a échoué ; l’erreur se propage.
|
||||
* **Pas de récupération anticipée.** Une entrée stockée est servie jusqu’à expiration de son TTL, et l’appel suivant paie l’aller-retour ; rien ne se rafraîchit en arrière-plan.
|
||||
* **Pas de regroupement.** Deux appels identiques concurrents font deux récupérations.
|
||||
* **Pas de TTL au-delà de 24 heures.** Un `ttlMs` supérieur, qu’il vienne du serveur ou de la configuration, est ramené à ce plafond au stockage (`mcp.client.caching.MAX_TTL_MS`), ce qui borne la durée pendant laquelle une entrée, si généreuse soit son indication, peut être servie.
|
||||
* Sur un **magasin partagé**, les clients sont en concurrence. Chaque client abandonne sa propre écriture lorsqu’une éviction a doublé la récupération en cours, mais un client *colocataire* peut toujours réécrire une entrée qu’une éviction qu’il n’a jamais vue avait supprimée ; et ce suivi des concurrences est lui-même borné : au-delà de 4 096 clés suivies, la garde de la clé la plus ancienne est abandonnée en premier. Les deux fenêtres sont acceptées, et refermées par le plafond de TTL ci-dessus.
|
||||
* **Pas de service d’une génération de protocole à l’autre.** Les entrées sont rattachées à la version de protocole négociée : sur un magasin persistant partagé, une session ne sert jamais une entrée écrite sous une autre version négociée (la même liste diffère réellement selon la génération, puisque le SDK retire les champs 2026 pour les sessions plus anciennes). L’éviction, de même, ne touche que les entrées de la génération courante ; les entrées d’une autre génération expirent simplement avec leur TTL.
|
||||
|
||||
### Lire les indications vous-même {#reading-the-hints-yourself}
|
||||
|
||||
Les indications sont aussi de simples champs sur chaque résultat pouvant être mis en cache (`result.ttl_ms` et `result.cache_scope`, déjà analysés), au cas où vous voudriez superposer votre propre suivi au cache intégré (ou le remplacer).
|
||||
|
||||
Face à un **serveur plus ancien** (protocole antérieur à 2026), les champs sont tout simplement absents de la liaison, et les modèles affichent leurs valeurs par défaut prudentes : `ttl_ms == 0` et `cache_scope == "private"`, périmé et non partagé, la bonne hypothèse pour un serveur qui n’a rien déclaré. Le cache traite une session historique de la même façon : les indications n’y sont jamais consultées (quelles que soient les clés présentes sur la liaison), seul `default_ttl_ms` s’applique, et sa valeur par défaut de `0` ne met rien en cache, de sorte qu’une connexion antérieure à 2026 se comporte exactement comme avant l’existence du cache. Si vous devez distinguer « le serveur a dit 0 » de « le serveur n’a rien dit », testez `"ttl_ms" in result.model_fields_set` : il n’est défini que lorsque le champ est réellement arrivé.
|
||||
|
||||
## Clients plus anciens {#older-clients}
|
||||
|
||||
Les clients sur des versions de protocole antérieures à 2026 ne voient jamais ni l’un ni l’autre de ces champs ; le SDK les retire à la sérialisation pour ces connexions. Configurez vos indications une fois pour toutes ; il n’y a rien de propre à une version à écrire.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* Six méthodes portent `ttlMs`/`cacheScope` ; le SDK leur donne par défaut `0`/`"private"`, périmé et non partagé, toujours sûr.
|
||||
* `cache_hints={method: CacheHint(...)}` à la construction (`MCPServer` comme `Server`) fixe des valeurs par méthode pour tout le serveur.
|
||||
* Un gestionnaire qui définit les champs sur son résultat surcharge le dictionnaire, champ par champ.
|
||||
* `"public"` est la promesse que le résultat est identique pour chaque appelant. Ce n’est pas un contrôle d’accès.
|
||||
* `Client` respecte les indications automatiquement : son cache de réponses est activé par défaut, sert les entrées fraîches au lieu de les récupérer à nouveau, et ne met rien en cache pour les serveurs (ou les sessions) qui ne fournissent aucune indication.
|
||||
* Par appel, `cache_mode="refresh"` récupère à nouveau et `"bypass"` saute le cache ; `cache=None` à la construction le désactive entièrement.
|
||||
154
i18n/fr/pages/client/callbacks.md
Normal file
154
i18n/fr/pages/client/callbacks.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
---
|
||||
translation:
|
||||
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
|
||||
tool: 1
|
||||
---
|
||||
# Fonctions de rappel du client {#client-callbacks}
|
||||
|
||||
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 {#a-server-that-asks}
|
||||
|
||||
Voici un serveur dont l’outil ne peut pas terminer tout seul :
|
||||
|
||||
```python title="server.py" hl_lines="16"
|
||||
--8<-- "docs_src/client_callbacks/tutorial001.py"
|
||||
```
|
||||
|
||||
* `ctx.elicit(...)` envoie une requête `elicitation/create` **au 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](../handlers/elicitation.md)** la couvre en détail. Cette page-ci se tient à l’autre bout de la liaison.
|
||||
|
||||
## La fonction de rappel d’élicitation {#the-elicitation-callback}
|
||||
|
||||
```python title="client.py" hl_lines="6-10 16-17"
|
||||
--8<-- "docs_src/client_callbacks/tutorial002.py"
|
||||
```
|
||||
|
||||
* Une fonction de rappel d’élicitation (elicitation) a pour signature `async (context, params) -> ElicitResult`.
|
||||
* `params.message` est la question. `params.requested_schema` est 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={...})`, ou `action="decline"`, ou `action="cancel"`. La seule autre option est `ErrorData(...)`, qui refuse la requête et fait échouer l’appel entier.
|
||||
* `context` est un `ClientRequestContext` : la `session` active, le `request_id` du serveur et les éventuelles `meta` qu’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](../handlers/elicitation.md)** montre le motif complet.
|
||||
|
||||
### Essayer {#try-it}
|
||||
|
||||
Appelez `issue_card` et observez les deux extrémités.
|
||||
|
||||
Votre fonction de rappel reçoit la question du serveur, déjà analysée :
|
||||
|
||||
```python
|
||||
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 :
|
||||
|
||||
```python
|
||||
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](../protocol-versions.md)**.
|
||||
|
||||
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](../handlers/multi-round-trip.md)** (multi-round-trip).
|
||||
|
||||
## Une fonction de rappel est une capacité {#a-callback-is-a-capability}
|
||||
|
||||
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 :
|
||||
|
||||
```python title="server.py" hl_lines="23-31"
|
||||
--8<-- "docs_src/client_callbacks/tutorial003.py"
|
||||
```
|
||||
|
||||
Connectez-vous avec seulement `elicitation_callback` et appelez-le :
|
||||
|
||||
```python
|
||||
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 :
|
||||
|
||||
```text
|
||||
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 {#the-deprecated-pair}
|
||||
|
||||
`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](../handlers/multi-round-trip.md)**). 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](../deprecated.md)**.
|
||||
|
||||
Vous avez encore besoin de ces fonctions de rappel pour parler aux serveurs qui n’ont pas migré. Les signatures :
|
||||
|
||||
```python title="client.py"
|
||||
--8<-- "docs_src/client_callbacks/tutorial004.py"
|
||||
```
|
||||
|
||||
* Une fonction de rappel d’échantillonnage reçoit le `CreateMessageRequestParams` complet (`messages`, `model_preferences`, `max_tokens`) et renvoie un `CreateMessageResult`. 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 {#the-notification-callbacks}
|
||||
|
||||
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](../handlers/logging.md)** 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 {#recap}
|
||||
|
||||
* 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_callback` et `list_roots_callback` fonctionnent 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_callback` et `message_handler` reçoivent des notifications. Ils ne déclarent rien.
|
||||
|
||||
Le premier argument de `Client(...)` est un objet transport. **[Transports client](transports.md)** couvre tous les types.
|
||||
155
i18n/fr/pages/client/identity-assertion.md
Normal file
155
i18n/fr/pages/client/identity-assertion.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0]
|
||||
tool: 1
|
||||
---
|
||||
# Assertion d’identité {#identity-assertion}
|
||||
|
||||
Un fournisseur OAuth ordinaire (**[Clients OAuth](oauth-clients.md)**) commence par poser une question au serveur MCP : *à quel serveur d’autorisation faites-vous confiance ?* Il suit la réponse où qu’elle mène, puis soit une personne se connecte, soit un secret pré-partagé en tient lieu.
|
||||
|
||||
Une entreprise ne veut voir ni l’un ni l’autre décidé serveur par serveur. Elle exploite déjà un fournisseur d’identité (Okta, Microsoft Entra ID, le vôtre) ; l’utilisateur s’y est déjà connecté ce matin ; et c’est l’unique endroit où l’équipe sécurité veut décider qui peut accéder à quoi. La [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), l’extension **Enterprise-Managed Authorization**, y déplace la décision. L’IdP signe un JWT de courte durée, un **Identity Assertion JWT Authorization Grant**, l’**ID-JAG** : une déclaration selon laquelle *cet utilisateur*, via *ce client*, peut accéder à *ce serveur MCP*. Le client l’échange contre un jeton d’accès ordinaire. Pas de navigateur, pas d’écran de consentement, pas d’enregistrement dynamique.
|
||||
|
||||
Cette page couvre les deux extrémités de cet échange. Le serveur MCP lui-même ne change jamais : il reste le serveur de ressources de **[Autorisation](../run/authorization.md)**, qui vérifie le jeton qui se présente, quel qu’il soit.
|
||||
|
||||
## Deux requêtes de jeton {#two-token-requests}
|
||||
|
||||
Deux autorités différentes sont en jeu, et bien les distinguer, c’est l’essentiel pour comprendre cette page. L’**IdP d’entreprise** est le fournisseur d’identité de votre organisation : il sait qui est l’employé, c’est là que réside la politique d’accès, et il émet l’ID-JAG. Le SDK ne lui parle jamais. Le **serveur d’autorisation MCP** est le même acteur que dans **[Autorisation](../run/authorization.md)** : l’émetteur nommé dans les métadonnées du serveur MCP, celui qui émet les jetons que ce serveur MCP accepte. Dans un flux OAuth ordinaire, ces deux rôles tiennent généralement dans une seule boîte. Ici ils sont deux, et tout le grant consiste en ce que le second accepte de faire confiance au premier.
|
||||
|
||||
Le client adresse une requête de jeton à chacun.
|
||||
|
||||
1. **Vers l’IdP d’entreprise.** Le client échange la connexion de l’utilisateur (son jeton d’identité OpenID Connect) contre l’ID-JAG. C’est un échange de jetons [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693), c’est entièrement l’API de votre IdP, et **le SDK ne l’effectue pas**. C’est vous qui le faites, dans une seule fonction de rappel (callback) asynchrone. C’est aussi là que se prend la décision de politique : un IdP qui dit non n’émet jamais l’ID-JAG, et il n’y a rien à présenter.
|
||||
2. **Vers le serveur d’autorisation MCP.** Le client présente l’ID-JAG sous le grant `jwt-bearer` de la [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) (`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, l’ID-JAG comme `assertion`) et reçoit le jeton d’accès. **C’est la requête que le SDK effectue**, et l’accepter est la seule chose que cette page ajoute à un serveur d’autorisation.
|
||||
|
||||
Tout ce qui suit concerne la seconde requête : le client qui l’envoie et le serveur d’autorisation qui y répond.
|
||||
|
||||
## Le client {#the-client}
|
||||
|
||||
**`IdentityAssertionOAuthProvider`** se trouve dans `mcp.client.auth.extensions.identity_assertion`. Comme tous les fournisseurs de **[Clients OAuth](oauth-clients.md)**, c’est un `httpx2.Auth` : construisez-en un, placez-le sur `auth=`, passez le `httpx2.AsyncClient` au transport.
|
||||
|
||||
```python title="client.py" hl_lines="49-50 53-61"
|
||||
--8<-- "docs_src/identity_assertion/tutorial001.py"
|
||||
```
|
||||
|
||||
Lisez-le en partant du bas.
|
||||
|
||||
* `main()` est le `main()` standard d’un client OAuth (**[Clients OAuth](oauth-clients.md)**), inchangé ligne pour ligne. C’est tout l’intérêt : une fois le fournisseur en place, rien en aval ne sait quel grant a produit le jeton.
|
||||
* Le fournisseur prend ce que les autres fournisseurs ne peuvent pas découvrir : un `client_id` et un `client_secret` que quelqu’un a **pré-enregistrés** auprès du serveur d’autorisation, la valeur `issuer` de ce serveur d’autorisation, et `assertion_provider`, une fonction de rappel asynchrone qui renvoie un ID-JAG tout neuf à la demande.
|
||||
* `storage` est le même protocole `TokenStorage`. Seules les deux méthodes de jeton sont appelées ; il n’y a pas d’enregistrement dynamique ici, donc pas de `client_info` à mémoriser.
|
||||
|
||||
### Le fournisseur d’assertion {#the-assertion-provider}
|
||||
|
||||
`fetch_id_jag(audience, resource)` est le seul code que vous écrivez. Il est attendu (await) une fois par échange de jeton, jamais à la construction, et seulement *après* que les métadonnées du serveur d’autorisation ont été récupérées et validées, si bien qu’un émetteur mal configuré ne laisse jamais fuiter une assertion. Ses deux arguments sont deux des claims avec lesquels l’ID-JAG doit être émis : `audience` est l’émetteur du serveur d’autorisation (le `aud` de l’ID-JAG) et `resource` est l’identifiant canonique du serveur MCP (le `resource` de l’ID-JAG). Le troisième, vous le détenez déjà : le claim `client_id` de l’ID-JAG doit nommer le `client_id` que vous avez donné au fournisseur, faute de quoi le serveur d’autorisation refuse l’échange.
|
||||
|
||||
`idp_issue_id_jag`, juste au-dessus, n’est **pas votre code**. Il tient lieu de fournisseur d’identité et signe l’assertion dans le processus même, pour que le fichier soit complet et que vous puissiez lire chaque claim que porte un ID-JAG. Un vrai `fetch_id_jag` effectue à la place la première requête de jeton de la section précédente : un échange de jetons [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) auprès de votre IdP, défini par le draft Identity Assertion JWT Authorization Grant dont la [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) définit un profil. Le jeton d’identité de l’utilisateur connecté y entre comme `subject_token`, le `requested_token_type` est l’URN propre à l’ID-JAG (`urn:ietf:params:oauth:token-type:id-jag`), `audience` et `resource` sont transmis tels quels, et la réponse porte l’ID-JAG. Cet échange, sous ces noms-là, est ce qu’il faut chercher dans la documentation de votre IdP.
|
||||
|
||||
!!! tip
|
||||
Un nouvel ID-JAG est demandé à chaque échange, et c’est voulu : c’est un grant à usage unique,
|
||||
valable quelques minutes, et le serveur d’autorisation de cette page refuse d’accepter deux fois
|
||||
le même. Ne le mettez pas en cache. C’est le jeton d’accès qu’il vous procure qui est réutilisé.
|
||||
|
||||
### L’émetteur relève de la configuration {#the-issuer-is-configuration}
|
||||
|
||||
Voici l’inversion. `OAuthClientProvider` demande au serveur de ressources quel serveur d’autorisation utiliser et suit la réponse où qu’elle mène. Ce fournisseur-ci s’y refuse : `issuer` est obligatoire, les métadonnées [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) sont récupérées depuis le chemin well-known de cet émetteur même, le point de terminaison de jeton doit se trouver sur l’origine de cet émetteur, et rien n’est jamais demandé au serveur de ressources.
|
||||
|
||||
L’extension ne l’exige pas ; c’est un choix délibérément plus strict. Ce client transporte deux choses qui valent d’être volées, un secret pré-enregistré et une assertion liée à une audience, et un client qui laisserait un serveur MCP compromis l’aiguiller vers le serveur d’autorisation d’un attaquant y posterait les deux. Épingler l’émetteur à la construction supprime purement et simplement cette conversation.
|
||||
|
||||
!!! warning
|
||||
La valeur `issuer` configurée est comparée au champ `issuer` du document de métadonnées par la
|
||||
comparaison de chaînes simple de la RFC 8414 §3.3 : caractère par caractère, barre oblique finale
|
||||
comprise, sans normalisation. Ne la devinez pas. Récupérez `/.well-known/oauth-authorization-server`
|
||||
auprès de votre serveur d’autorisation et copiez la valeur `issuer` qu’il renvoie. Pour le serveur
|
||||
d’autorisation de cette page, c’est `https://auth.example.com/`, avec la barre oblique, parce que
|
||||
son émetteur a été construit à partir d’un objet URL pydantic. Une discordance arrête le flux
|
||||
sur `OAuthFlowError: Authorization server metadata issuer
|
||||
mismatch` avant qu’un seul identifiant ou une seule assertion ne soit envoyé.
|
||||
|
||||
### Un client confidentiel {#a-confidential-client}
|
||||
|
||||
`client_secret` est obligatoire ; sans lui, le constructeur lève `ValueError`. Le profil IETF sous-jacent à la [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) réserve ce grant aux clients confidentiels, la SEP-990 exige que le client s’authentifie, et ce SDK fait respecter les deux en imposant un secret partagé. `token_endpoint_auth_method` choisit par où il transite : `client_secret_post` (la valeur par défaut, dans le corps du formulaire) ou `client_secret_basic` (un en-tête HTTP Basic). Le profil autorise aussi `private_key_jwt` ; ce fournisseur ne le prend pas en charge.
|
||||
|
||||
!!! tip
|
||||
Lisez `client_secret` depuis l’environnement ou un gestionnaire de secrets, jamais depuis le dépôt de code.
|
||||
|
||||
### Ce que le fournisseur fait pour vous {#what-the-provider-does-for-you}
|
||||
|
||||
La première requête part sans authentification, et le `401` du serveur démarre le flux.
|
||||
|
||||
1. **Découverte.** Il récupère les métadonnées du serveur d’autorisation depuis le chemin well-known [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) de l’émetteur configuré, vérifie que la valeur `issuer` du document correspond, et vérifie que le point de terminaison de jeton se trouve sur l’origine de l’émetteur.
|
||||
2. **L’assertion.** Il attend (await) votre `assertion_provider`.
|
||||
3. **Échange.** Il envoie en POST le grant `jwt-bearer` au point de terminaison de jeton, stocke le `OAuthToken`, et rejoue votre requête d’origine avec `Authorization: Bearer ...`.
|
||||
|
||||
Un `403` dont le `WWW-Authenticate` nomme `insufficient_scope` relance les étapes 2 et 3 avec l’union de votre `scope` et de celui du défi. (`scope` n’est jamais qu’une demande ; le serveur d’autorisation de cette page accorde ce que dit l’ID-JAG et rien d’autre.) Il n’y a de jeton d’actualisation nulle part ici : quand le jeton d’accès expire, le `401` suivant fait émettre un nouvel ID-JAG et relance l’échange, et c’est *là* le levier que détient l’IdP. Les échecs sont les deux mêmes exceptions que dans le reste de **[Clients OAuth](oauth-clients.md)** : `OAuthFlowError` pour la découverte et la validation, sa sous-classe `OAuthTokenError` quand le point de terminaison de jeton dit non.
|
||||
|
||||
## Le serveur d’autorisation {#the-authorization-server}
|
||||
|
||||
La plupart du temps, vous vous arrêtez ici. Le serveur d’autorisation MCP est le produit de quelqu’un d’autre, accepter les ID-JAG est une option de sa configuration à activer, et la moitié de la [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) qui revient au SDK est le client ci-dessus.
|
||||
|
||||
Le SDK peut aussi *être* le serveur d’autorisation : `create_auth_routes` renvoie les routes du serveur d’autorisation sous forme d’une liste que n’importe quelle application Starlette peut monter, et c’est ainsi que `examples/servers/simple-auth/` dans le dépôt en fait tourner un. La SEP-990 ajoute un drapeau et une méthode à cette surface :
|
||||
|
||||
```python title="auth_server.py" hl_lines="48-50 105-107"
|
||||
--8<-- "docs_src/identity_assertion/tutorial002.py"
|
||||
```
|
||||
|
||||
* `identity_assertion_enabled=True` conditionne tout. Désactivé, ce qui est la valeur par défaut, `/token` répond à ce grant par `unsupported_grant_type` même si vous avez implémenté le hook, et les métadonnées n’en font pas mention. Activé, les métadonnées gagnent le type de grant `jwt-bearer` et listent `urn:ietf:params:oauth:grant-profile:id-jag` dans `authorization_grant_profiles_supported`, le champ par lequel l’extension annonce sa prise en charge. (Le client de ce SDK ne le lit jamais : il est provisionné pour un seul émetteur et demande, tout simplement.)
|
||||
* **`exchange_identity_assertion`** est le hook. Avant qu’il ne s’exécute, le SDK a authentifié le client, refusé les clients publics, et refusé les clients dont l’enregistrement ne liste pas le grant. Vous recevez un `IdentityAssertionParams` (la valeur `assertion` brute, les `scopes` et `resource` demandés) et renvoyez un simple `OAuthToken`.
|
||||
* L’enregistrement dynamique des clients refuse ce grant sans condition, si bien que `get_client` sert ici un client provisionné à la main. Un client ID-JAG ne peut pas se faire exister en s’enregistrant lui-même.
|
||||
* La moitié de la classe est faite de refus. `OAuthAuthorizationServerProvider` est le serveur d’autorisation *tout entier*, il réclame donc aussi le flux authorization code ; un serveur qui connecte aussi des utilisateurs implémente ces méthodes pour de bon, et celui-ci n’a qu’une seule porte.
|
||||
|
||||
!!! warning
|
||||
Le SDK ne décode jamais l’assertion : seul votre déploiement sait à quel IdP il fait confiance et
|
||||
quelles clés cet IdP publie, donc tout ce qui se trouve dans `exchange_identity_assertion` est
|
||||
déterminant. Vérifiez la signature par rapport aux clés publiées de l’IdP (son JWKS ; le secret
|
||||
partagé ici est celui de la démo), ainsi que `iss` et `exp`, selon la [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) §3. Exigez
|
||||
que le `typ` de l’en-tête JWT soit `oauth-id-jag+jwt`, le garde-fou du profil contre le rejeu
|
||||
d’un autre JWT comme grant. Exigez que `aud` soit votre propre émetteur. Exigez que le claim
|
||||
`client_id` de l’ID-JAG soit égal au client que le gestionnaire (handler) a authentifié, et que
|
||||
son claim `resource` nomme une ressource que vous servez réellement. Suivez `jti` jusqu’à la
|
||||
valeur `exp` de l’assertion pour qu’elle ne soit acceptée qu’une fois. Et tirez les scopes
|
||||
accordés et, surtout, le `resource` du jeton émis de l’ID-JAG validé, jamais de la requête :
|
||||
`params.resource` est ce que le client a tapé, quoi que ce soit. Les règles de traitement
|
||||
complètes sont dans la
|
||||
[spécification Enterprise-Managed Authorization](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization).
|
||||
|
||||
Rejetez une mauvaise assertion avec `TokenError("invalid_grant", ...)`. L’autre code d’erreur de ce flux est `invalid_target` : un ID-JAG qui nomme une ressource que vous ne servez pas est refusé avec lui, et c’est ce qui empêche ce serveur d’émettre des jetons pour celle de quelqu’un d’autre. Et les scopes accordés viennent du claim `scope` de l’ID-JAG (une assertion qui n’en a pas est refusée elle aussi) ; le vôtre pourrait plutôt faire correspondre les groupes de l’utilisateur.
|
||||
|
||||
Et remarquez ce que le `OAuthToken` renvoyé ne porte pas : un jeton d’actualisation. L’IdP décide combien de temps cet utilisateur garde l’accès en décidant d’émettre ou non le prochain ID-JAG. Un jeton d’actualisation émis ici reprendrait en douce cette décision à l’IdP.
|
||||
|
||||
!!! info
|
||||
Un serveur qui embarque encore son serveur d’autorisation avec `auth_server_provider=` atteint le
|
||||
même code via `AuthSettings(identity_assertion_enabled=True)`. **[Autorisation](../run/authorization.md)** explique pourquoi
|
||||
les nouveaux serveurs ne devraient pas commencer par là.
|
||||
|
||||
!!! check
|
||||
Reliez les deux fichiers de cette page et tout le grant tient en un seul `POST /token` :
|
||||
|
||||
```text
|
||||
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
|
||||
assertion=eyJhbGciOiJIUzI1NiIsInR5cCI6Im9hdXRoLWlkLWphZytqd3QifQ...
|
||||
client_id=finance-agent
|
||||
resource=http://localhost:8001/mcp
|
||||
scope=notes:read
|
||||
client_secret=finance-agent-secret
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
{"access_token": "mcp_...", "token_type": "Bearer", "expires_in": 300, "scope": "notes:read"}
|
||||
```
|
||||
|
||||
Pas de `/authorize`, pas de `/register`, pas de récupération des métadonnées de ressource
|
||||
protégée. Les seules requêtes sur la liaison sont celle qui a provoqué le `401`, la récupération
|
||||
well-known, cet échange, puis le trafic MCP ordinaire avec le jeton porteur attaché. Et le `sub`
|
||||
que votre validateur a lu dans l’ID-JAG est exactement ce que `get_access_token().subject`
|
||||
rapporte à l’intérieur d’un outil.
|
||||
|
||||
### Essayer {#try-it}
|
||||
|
||||
`examples/stories/identity_assertion/` dans le dépôt du SDK, c’est cette page exécutée pour de bon : le même validateur `exchange_identity_assertion`, un serveur MCP protégé par ses jetons, un IdP de substitution et le client, dans un seul programme qui se vérifie lui-même. `uv run python -m stories.identity_assertion.client --http` exécute tout l’échange et vérifie par assertion que l’utilisateur nommé par l’IdP est bien celui que voit l’outil.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* La [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) laisse le fournisseur d’identité de l’entreprise, et non l’utilisateur final, décider quels serveurs MCP un client peut atteindre. L’IdP signe cette décision dans un **ID-JAG**.
|
||||
* Obtenir l’ID-JAG est un échange de jetons [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) auprès de *votre IdP*, et le SDK ne l’effectue pas. Le présenter au serveur d’autorisation MCP relève du grant `jwt-bearer` de la [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523), et le SDK en assure les deux côtés.
|
||||
* `IdentityAssertionOAuthProvider` est un `httpx2.Auth` de plus : un client confidentiel pré-enregistré, un `issuer` épinglé, et une fonction de rappel `assertion_provider(audience, resource)`. Pas de navigateur, pas d’enregistrement, pas de jeton d’actualisation.
|
||||
* Le serveur d’autorisation n’est jamais découvert à partir du serveur de ressources. Configurez `issuer` avec exactement la chaîne que sert son document de métadonnées ; la comparaison se fait caractère par caractère.
|
||||
* Côté serveur, `identity_assertion_enabled=True` plus `exchange_identity_assertion`. Le SDK authentifie le client et conditionne le grant ; valider l’ID-JAG vous revient entièrement, et le jeton émis est lié au `resource` de l’ID-JAG, pas à celui de la requête.
|
||||
|
||||
Le seul acteur auquel cette page n’a jamais touché est le serveur MCP. Ce qu’il fait du jeton que vous venez d’émettre, il le faisait déjà dans **[Autorisation](../run/authorization.md)**.
|
||||
217
i18n/fr/pages/client/index.md
Normal file
217
i18n/fr/pages/client/index.md
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ebef1e7a0df854f4, a4c687d3d627d516, 8e79141fc2985342, b345dd05b9c3c7ab, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
|
||||
tool: 1
|
||||
---
|
||||
# Le client {#the-client}
|
||||
|
||||
Un **`Client`** est le moyen par lequel un programme Python dialogue avec un serveur MCP.
|
||||
|
||||
C’est un seul objet avec un seul cycle de vie : vous le construisez, vous entrez dans `async with`, vous appelez des méthodes. Chaque verbe du protocole (lister les outils, en appeler un, lire une ressource, rendre un prompt) est une méthode `async` de cet objet qui renvoie un résultat typé.
|
||||
|
||||
## Votre premier client {#your-first-client}
|
||||
|
||||
```python title="client.py" hl_lines="14-18"
|
||||
--8<-- "docs_src/client/tutorial001.py"
|
||||
```
|
||||
|
||||
Le serveur en haut n’est là que pour vous donner quelque chose à quoi vous connecter. Le client, ce sont les cinq lignes surlignées.
|
||||
|
||||
* `Client(mcp)` reçoit **l’objet serveur lui-même**. C’est le transport en mémoire : pas de sous-processus, pas de port, pas de HTTP. C’est ainsi que se connectent tous les exemples de cette page, et tous les tests que vous écrivez.
|
||||
* `async with` est le **cycle de vie**. Y entrer connecte et négocie ; en sortir déconnecte. Il n’y a pas de paire `connect()` / `close()`, et un `Client` ne peut pas être réutilisé une fois le bloc terminé.
|
||||
* À l’intérieur du bloc, les informations de connexion sont déjà là, sous forme de simples propriétés.
|
||||
|
||||
### Ce que vous pouvez passer à `Client` {#what-you-can-pass-to-client}
|
||||
|
||||
`Client` prend un seul argument positionnel et déduit le transport de son type :
|
||||
|
||||
* Une instance de `MCPServer` (ou du `Server` bas niveau) : connexion **dans le processus**.
|
||||
* Une chaîne d’URL (`Client("http://localhost:8000/mcp")`) : Streamable HTTP, la voie de production.
|
||||
* Un **transport** : tout ce sur quoi vous pouvez faire `async with ... as (read, write)`, comme `stdio_client(...)` qui enveloppe un sous-processus.
|
||||
|
||||
Tout le reste de cette page est identique pour les trois. Les en-têtes, les sous-processus, les délais d’expiration et le protocole `Transport` ont leur propre page : **[Transports côté client](transports.md)**.
|
||||
|
||||
### Ce que porte un client connecté {#whats-on-a-connected-client}
|
||||
|
||||
Quatre propriétés en lecture seule, renseignées dès que vous entrez dans le bloc :
|
||||
|
||||
* `client.server_info` : l’identité du serveur, ou `None` pour un serveur de génération 2026 qui n’en déclare pas (les serveurs python-sdk le font par défaut). Ici, `server_info.name` vaut `"Bookshop"` et `server_info.version` est ce que le serveur déclare.
|
||||
* `client.server_capabilities` : ce que le serveur sait faire (`tools`, `resources`, `prompts`, `completions`, ...). Une capacité que le serveur n’a pas vaut `None`.
|
||||
* `client.protocol_version` : la version du protocole sur laquelle les deux côtés se sont mis d’accord. Ici, c’est `"2026-07-28"`.
|
||||
* `client.instructions` : la chaîne `instructions=` du serveur, ou `None` s’il n’en a pas défini.
|
||||
|
||||
Vous n’avez jamais choisi de version du protocole. Par défaut, le `Client` sonde le serveur et se rabat sur la poignée de main (handshake) classique avec les plus anciens, si bien qu’un seul client fonctionne avec un serveur de n’importe quelle génération. Lorsque vous avez besoin de contrôler cela, tous les détails sont dans **[Versions du protocole](../protocol-versions.md)**.
|
||||
|
||||
!!! tip
|
||||
`client.session` est la `ClientSession` sous-jacente, l’échappatoire bas niveau.
|
||||
Vous n’en aurez besoin pour rien sur cette page.
|
||||
|
||||
## Lister les outils {#listing-tools}
|
||||
|
||||
```python title="client.py" hl_lines="15-20"
|
||||
--8<-- "docs_src/client/tutorial002.py"
|
||||
```
|
||||
|
||||
`list_tools()` renvoie un `ListToolsResult` ; les outils sont dans `.tools`. Chacun est la définition complète qu’un hôte transmettrait à un modèle :
|
||||
|
||||
```python
|
||||
tool.name # 'search_books'
|
||||
tool.title # 'Search the catalog'
|
||||
tool.description # 'Search the catalog by title or author.'
|
||||
```
|
||||
|
||||
et `tool.input_schema` est le JSON Schema que le serveur a dérivé des annotations de type de la fonction :
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"title": "Query", "type": "string"},
|
||||
"limit": {"default": 10, "title": "Limit", "type": "integer"}
|
||||
},
|
||||
"required": ["query"],
|
||||
"title": "search_booksArguments"
|
||||
}
|
||||
```
|
||||
|
||||
Ce schéma est tout ce dont une interface a besoin pour afficher un formulaire d’arguments, et tout ce dont un modèle a besoin pour produire des arguments valides.
|
||||
|
||||
!!! tip
|
||||
`title` est facultatif, donc une interface qui présente des outils à un humain doit choisir : le `title` s’il existe,
|
||||
le `name` sinon. `from mcp.shared.metadata_utils import get_display_name` fait exactement cela,
|
||||
pour les outils, les ressources, les modèles de ressource et les prompts.
|
||||
|
||||
## Appeler un outil {#calling-a-tool}
|
||||
|
||||
`call_tool(name, arguments)` exécute l’outil et vous renvoie un `CallToolResult`.
|
||||
|
||||
```python title="client.py" hl_lines="26-33"
|
||||
--8<-- "docs_src/client/tutorial003.py"
|
||||
```
|
||||
|
||||
Le `lookup_book` du serveur renvoie un `Book` Pydantic. Voici ce que voit le client :
|
||||
|
||||
```python
|
||||
result.content # [TextContent(type='text', text='{\n "title": "Dune",\n "author": "Frank Herbert",\n "year": 1965\n}')]
|
||||
result.structured_content # {'title': 'Dune', 'author': 'Frank Herbert', 'year': 1965}
|
||||
result.is_error # False
|
||||
```
|
||||
|
||||
Une valeur de retour, trois choses à lire. Chacune a un consommateur différent.
|
||||
|
||||
### `content` : ce que lit le modèle {#content-what-the-model-reads}
|
||||
|
||||
`content` est une `list` de **blocs de contenu**, et un bloc de contenu est une union : `TextContent`, `ImageContent`, `AudioContent`, `ResourceLink` ou `EmbeddedResource`. Un outil peut en renvoyer plusieurs, de natures différentes.
|
||||
|
||||
C’est pourquoi `main` restreint le type avec `isinstance(block, TextContent)` avant de toucher à `block.text`. Remarquez qu’il n’y a pas de `.text` en dehors du `isinstance` : le vérificateur de types ne le permettrait pas, car `ImageContent` a `.data`, pas `.text`. L’union est honnête sur ce qu’un outil a le droit de vous envoyer ; votre code devrait l’être aussi.
|
||||
|
||||
### `structured_content` : ce que lit votre application {#structured_content-what-your-application-reads}
|
||||
|
||||
`structured_content` est la valeur de retour de l’outil au format JSON, conforme au `output_schema` déclaré par l’outil. Pas d’analyse de chaînes, pas de devinettes.
|
||||
|
||||
Quand les deux sont présents, ils disent volontairement deux fois la même chose : `content` est pour un modèle, `structured_content` pour du code. D’où vient la moitié structurée, et comment la contrôler, c’est le sujet de la page **[Sortie structurée](../servers/structured-output.md)**.
|
||||
|
||||
### `is_error` : si l’outil a échoué {#is_error-whether-the-tool-failed}
|
||||
|
||||
Un outil qui lève une exception ne lève **rien** dans votre client. Il revient sous la forme d’un résultat ordinaire avec `is_error=True`.
|
||||
|
||||
!!! check
|
||||
Demandez `"Solaris"` à `lookup_book` (un titre qui n’est pas au catalogue) et la fonction lève
|
||||
`ValueError`. L’appel revient pourtant normalement :
|
||||
|
||||
```python
|
||||
result.is_error # True
|
||||
result.content # [TextContent(type='text', text="Error executing tool lookup_book: No book titled 'Solaris' in the catalog.")]
|
||||
result.structured_content # None
|
||||
```
|
||||
|
||||
Le message de l’exception a atterri dans `content`, où le **modèle** peut le lire et réessayer. C’est
|
||||
délibéré : une erreur d’outil fait partie de la conversation, ce n’est pas un plantage. Regardez toujours `is_error`
|
||||
avant de faire confiance à `structured_content`.
|
||||
|
||||
!!! warning
|
||||
`is_error=True` couvre plus que vos propres `raise`. Demandez un outil que le serveur n’a même pas
|
||||
(`call_tool("does_not_exist", {})`) et rien n’est levé. Vous obtenez la même forme en retour :
|
||||
`is_error=True` avec `Unknown tool: does_not_exist` dans `content`. Une méthode de `Client` ne lève
|
||||
`MCPError` que lorsque le serveur répond par une **erreur** JSON-RPC au lieu d’un résultat, et
|
||||
**[Gérer les erreurs](../servers/handling-errors.md)** explique quand un serveur produit l’une ou l’autre.
|
||||
|
||||
## Ressources {#resources}
|
||||
|
||||
Les verbes des ressources vont par paires : deux façons de lister, une façon de lire.
|
||||
|
||||
```python title="client.py" hl_lines="22-31"
|
||||
--8<-- "docs_src/client/tutorial004.py"
|
||||
```
|
||||
|
||||
* `list_resources()` renvoie les ressources **concrètes**, celles qui ont un URI fixe. Ici : `['catalog://genres']`.
|
||||
* `list_resource_templates()` renvoie les ressources **paramétrées**. Ici : `['catalog://genres/{genre}']`. Ce sont deux listes distinctes parce qu’un modèle n’est pas lisible tant que vous ne l’avez pas rempli.
|
||||
* `read_resource(uri)` prend un URI sous forme de simple `str` et fonctionne sur les deux : passez `"catalog://genres/poetry"` et le serveur le fait correspondre au modèle.
|
||||
|
||||
`read_resource` renvoie `contents`, une liste de `TextResourceContents` ou de `BlobResourceContents`. Même idée que pour le contenu des outils : restreignez le type avec `isinstance`, puis lisez `.text` (ou `.blob`).
|
||||
|
||||
Un client peut aussi être prévenu quand une ressource change. Sur les connexions de génération 2025, c’est `subscribe_resource(uri)` / `unsubscribe_resource(uri)` — une paire de méthodes que `MCPServer` n’implémente pas, si bien que sur la liaison en version 2026-07-28 (où ces verbes n’existent plus) la requête reçoit en réponse `-32601`, *Method not found*. Le remplaçant en version 2026 est un flux `subscriptions/listen`, que `MCPServer` sert *bel et bien* — `server_capabilities.resources.subscribe` y vaut `True` — et sa consommation avec `client.listen(...)` fait l’objet de la page **[Abonnements](subscriptions.md)** de cette section.
|
||||
|
||||
## Prompts {#prompts}
|
||||
|
||||
```python title="client.py" hl_lines="15-20"
|
||||
--8<-- "docs_src/client/tutorial005.py"
|
||||
```
|
||||
|
||||
`list_prompts()` vous dit ce que le serveur propose et ce dont chaque prompt a besoin :
|
||||
|
||||
```python
|
||||
prompt.name # 'recommend'
|
||||
prompt.title # 'Recommend a book'
|
||||
prompt.arguments # [PromptArgument(name='genre', required=True)]
|
||||
```
|
||||
|
||||
`get_prompt(name, arguments)` le rend. Le dictionnaire d’arguments est `str -> str` : les arguments de prompt sont toujours des chaînes. Le résultat est `messages`, une liste de `PromptMessage`, chacun avec un `role` et un bloc `content` :
|
||||
|
||||
```python
|
||||
message.role # 'user'
|
||||
message.content # TextContent(type='text', text='Recommend one poetry book from the catalog and say why.')
|
||||
```
|
||||
|
||||
Un hôte transmet ces messages tels quels au modèle. C’est toute la fonctionnalité.
|
||||
|
||||
## Complétions {#completions}
|
||||
|
||||
Un serveur doté d’un gestionnaire (handler) de complétion peut compléter automatiquement les arguments des prompts et des modèles de ressource au fil de la saisie de l’utilisateur.
|
||||
|
||||
```python title="client.py" hl_lines="27-31"
|
||||
--8<-- "docs_src/client/tutorial006.py"
|
||||
```
|
||||
|
||||
* `ref` indique *quel* prompt ou modèle vous remplissez : un `PromptReference` ou un `ResourceTemplateReference`.
|
||||
* `argument` vaut `{"name": ..., "value": ...}` : l’argument et ce que l’utilisateur a saisi jusqu’ici.
|
||||
|
||||
La réponse se trouve dans `result.completion.values`. Tapez `"p"` et le serveur revient avec `['poetry']`. Le côté serveur, et la façon dont un gestionnaire utilise les *autres* arguments déjà remplis pour affiner ses suggestions, c’est la page **[Complétions](../servers/completions.md)**.
|
||||
|
||||
## Pagination {#pagination}
|
||||
|
||||
Chaque méthode `list_*` accepte un argument nommé `cursor=` et chaque résultat porte un `next_cursor`. Quand `next_cursor` vaut `None`, vous avez tout.
|
||||
|
||||
```python title="client.py" hl_lines="22-30"
|
||||
--8<-- "docs_src/client/tutorial007.py"
|
||||
```
|
||||
|
||||
Cette boucle est correcte face à n’importe quel serveur. `MCPServer` renvoie tout en une seule page, donc `next_cursor` vaut `None` et la boucle s’exécute une fois, ce qui explique que la plupart du code ne l’écrive jamais. Les serveurs qui paginent réellement, et les règles auxquelles obéissent les curseurs, sont dans **[Pagination](../advanced/pagination.md)**.
|
||||
|
||||
## Dans les tests {#in-tests}
|
||||
|
||||
`Client(mcp)`, sans processus ni port, est déjà un banc de test pour votre serveur.
|
||||
|
||||
Il existe un drapeau du constructeur conçu pour cela : `Client(mcp, raise_exceptions=True)`. Il n’a d’effet que sur les connexions en mémoire, et **[Tests](../get-started/testing.md)** est la page qui l’explique et construit tout le modèle autour de lui.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* `Client(x)` se connecte en mémoire à un objet serveur, en Streamable HTTP à une chaîne d’URL, et à tout le reste via un transport.
|
||||
* `async with` est tout le cycle de vie. À l’intérieur, `server_capabilities` et `protocol_version` sont déjà renseignés ; `server_info` et `instructions` le sont aussi lorsque le serveur les fournit.
|
||||
* `list_tools()` vous donne le `name`, le `title`, la `description` et le `input_schema` de chaque outil.
|
||||
* `call_tool()` renvoie `content` pour le modèle, `structured_content` pour votre code, et `is_error`. Un outil qui lève une exception est un résultat, pas une exception.
|
||||
* `content` est une union de types de blocs ; restreignez le type avec `isinstance` avant de lire.
|
||||
* `list_resources` / `list_resource_templates` / `read_resource`, `list_prompts` / `get_prompt` et `complete` complètent la liste des verbes.
|
||||
* Chaque `list_*` accepte `cursor=` ; bouclez jusqu’à ce que `next_cursor` vaille `None`.
|
||||
|
||||
Ce qu’un serveur peut demander au *client*, et la façon d’y répondre, c’est **[Fonctions de rappel du client](callbacks.md)**.
|
||||
152
i18n/fr/pages/client/oauth-clients.md
Normal file
152
i18n/fr/pages/client/oauth-clients.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
---
|
||||
translation:
|
||||
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b]
|
||||
tool: 1
|
||||
---
|
||||
# Clients OAuth {#oauth-clients}
|
||||
|
||||
Certains serveurs MCP sont protégés. Envoyez-leur une requête sans jeton et ils répondent `401 Unauthorized`.
|
||||
|
||||
**`OAuthClientProvider`** est le moyen d’obtenir ce jeton. Ce n’est pas du tout un objet MCP. C’est un `httpx2.Auth`, le hook standard de httpx2 pour « faire quelque chose à chaque requête ». Vous l’attachez à un `httpx2.AsyncClient`, vous confiez ce client au transport Streamable HTTP, et vous n’y pensez plus.
|
||||
|
||||
Cette page couvre le côté client. Pour que votre propre serveur exige un jeton, voyez **[Autorisation](../run/authorization.md)**.
|
||||
|
||||
## Le fournisseur {#the-provider}
|
||||
|
||||
```python title="client.py" hl_lines="44-54"
|
||||
--8<-- "docs_src/oauth_clients/tutorial001.py"
|
||||
```
|
||||
|
||||
Vous lui donnez quatre choses :
|
||||
|
||||
* `server_url` : le point de terminaison MCP auquel vous vous connectez. Le fournisseur découvre tout le reste à partir de lui.
|
||||
* `client_metadata` : ce que vous saisiriez dans le formulaire « enregistrer une application » d’un serveur d’autorisation.
|
||||
* `storage` : là où les jetons vivent entre deux exécutions.
|
||||
* `redirect_handler` et `callback_handler` : les deux moments où un humain intervient.
|
||||
|
||||
Rien d’autre dans le fichier ne mentionne OAuth. `main()` ne voit jamais un jeton.
|
||||
|
||||
### Métadonnées du client {#client-metadata}
|
||||
|
||||
`OAuthClientMetadata` est le véritable document d’enregistrement de la [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591), sous forme de modèle Pydantic.
|
||||
|
||||
Vous définissez trois champs. Les valeurs par défaut remplissent le reste : `grant_types` vaut déjà `["authorization_code", "refresh_token"]` et `response_types` vaut déjà `["code"]`, ce qui correspond exactement au flux qu’exécute ce fournisseur.
|
||||
|
||||
!!! check
|
||||
Comme c’est un modèle Pydantic, il valide **avant qu’un seul octet ne parte sur le réseau**.
|
||||
Omettez `redirect_uris` et la construction échoue immédiatement avec une `ValidationError` qui
|
||||
nomme le champ :
|
||||
|
||||
```text
|
||||
redirect_uris
|
||||
Field required [type=missing, input_value={'client_name': 'Bookshop Agent'}, input_type=dict]
|
||||
```
|
||||
|
||||
Aucun navigateur ouvert, aucun enregistrement à moitié terminé laissé derrière sur le serveur d’autorisation.
|
||||
|
||||
### Stockage des jetons {#token-storage}
|
||||
|
||||
**`TokenStorage`** est un `Protocol` avec quatre méthodes asynchrones. Vous n’héritez de rien ; écrivez les méthodes et n’importe quelle classe devient un magasin de jetons :
|
||||
|
||||
* `get_tokens` / `set_tokens` conservent l’objet `OAuthToken` : jeton d’accès, jeton d’actualisation, expiration, portée.
|
||||
* `get_client_info` / `set_client_info` conservent l’objet `OAuthClientInformationFull` que le serveur d’autorisation a émis lorsque le fournisseur vous a enregistré, y compris votre `client_id`.
|
||||
|
||||
La version en mémoire ci-dessus fonctionne. Elle oublie aussi tout quand le processus se termine, si bien que l’exécution suivante refait toute la procédure. Persistez-la dans un fichier ou dans le trousseau de votre plateforme et l’exécution suivante est silencieuse.
|
||||
|
||||
!!! tip
|
||||
Stockez `client_info`, pas seulement les jetons. Le fournisseur s’enregistre dynamiquement la première fois qu’il
|
||||
ne trouve aucun `client_info` stocké. Jetez-le et vous créez un nouvel enregistrement à chaque exécution.
|
||||
|
||||
### Les deux gestionnaires {#the-two-handlers}
|
||||
|
||||
Le flux du code d’autorisation a besoin d’un humain exactement une fois : quelqu’un doit se connecter et cliquer sur « autoriser ».
|
||||
|
||||
* **`redirect_handler`** est attendu (await) avec l’URL d’autorisation entièrement construite. Le `client_id`, le `redirect_uri`, le `state` et le défi PKCE y figurent déjà. Votre seul travail est d’y amener un navigateur. Une application de bureau appelle `webbrowser.open` ; ce fichier l’affiche.
|
||||
* **`callback_handler`** est attendu ensuite. Il patiente jusqu’à ce que l’utilisateur revienne sur votre `redirect_uri` et renvoie les paramètres de requête de cette redirection sous la forme d’un `AuthorizationCodeResult`.
|
||||
|
||||
Un vrai client fait tourner un petit serveur HTTP local sur l’URI de redirection au lieu d’appeler `input()`. La forme est identique : recevoir la redirection, rendre `code`, `state` et `iss`.
|
||||
|
||||
!!! warning
|
||||
Transmettez `state` et `iss` exactement tels qu’ils sont arrivés. Le fournisseur compare `state` à celui
|
||||
qu’il a généré et `iss` à l’émetteur qu’il a découvert, et refuse toute divergence. Ce sont les défenses
|
||||
contre le CSRF et contre la confusion de serveurs (mix-up).
|
||||
|
||||
### Dans le `Client` {#into-the-client}
|
||||
|
||||
Regardez `main()`. Le fournisseur va sur le **client httpx2**, le client httpx2 va dans `streamable_http_client(url, http_client=...)`, et ce transport va dans `Client`.
|
||||
|
||||
`streamable_http_client` n’a pas de mot-clé `auth=`. Tout ce qui relève du niveau HTTP (authentification, en-têtes, délais d’expiration, proxys) appartient au `httpx2.AsyncClient` que vous apportez. Cette superposition de couches est décrite dans **[Transports client](transports.md)**.
|
||||
|
||||
## Ce que le fournisseur fait pour vous {#what-the-provider-does-for-you}
|
||||
|
||||
La première fois que `Client` envoie une requête, le serveur répond `401`. Le fournisseur prend le relais :
|
||||
|
||||
1. **Découverte.** Il lit l’en-tête `WWW-Authenticate`, récupère les Protected Resource Metadata du serveur depuis `/.well-known/oauth-protected-resource`, apprend quel serveur d’autorisation protège cette ressource, et récupère les métadonnées de *ce* serveur-là.
|
||||
2. **Enregistrement.** Rien dans le stockage ? Il vous enregistre dynamiquement avec votre `OAuthClientMetadata` et stocke le résultat.
|
||||
3. **Autorisation.** Il génère la paire PKCE et un `state`, construit l’URL d’autorisation, attend votre `redirect_handler`, puis attend votre `callback_handler` pour obtenir le code.
|
||||
4. **Échange.** Il échange le code contre un `OAuthToken`, le stocke, et rejoue votre requête d’origine avec `Authorization: Bearer ...`.
|
||||
|
||||
Après cela, il se fait discret. Les jetons sortent du stockage, un jeton d’accès expiré est actualisé avec le jeton d’actualisation, et ce n’est que lorsque rien de tout cela ne fonctionne qu’il relance le flux.
|
||||
|
||||
Vous n’avez rien écrit de tout cela. Il reste deux arguments nommés (`client_metadata_url` et `validate_resource_url`), et ce fichier n’a besoin d’aucun des deux. `client_metadata_url` est celui qui mérite d’être connu ; il a sa propre section plus bas.
|
||||
|
||||
### Essayer {#try-it}
|
||||
|
||||
La plupart des exemples de cette documentation se vérifient avec un `Client(server)` en mémoire. Pas celui-ci : tout l’intérêt du flux est un `401` HTTP, et il n’y a pas de HTTP entre un client en mémoire et son serveur.
|
||||
|
||||
Le dépôt fournit la version réelle. `examples/servers/simple-auth/` exécute un serveur d’autorisation autonome et un serveur MCP protégé ; `examples/clients/simple-auth-client/` est le client de cette page devenu une petite CLI. Son README donne les deux commandes : démarrez les serveurs, lancez le client contre eux, et vous voyez défiler les quatre étapes.
|
||||
|
||||
## Client ID Metadata Documents {#client-id-metadata-documents}
|
||||
|
||||
La révision 2026-07-28 de la spécification rend obsolète l’enregistrement dynamique des clients au profit des **Client ID Metadata Documents** (CIMD). Au lieu d’envoyer par POST un nouvel enregistrement à chaque serveur d’autorisation qu’il rencontre, votre client publie un unique document JSON le décrivant à une URL HTTPS stable, et cette URL *est* son `client_id`. Le serveur d’autorisation récupère le document ; le fournisseur n’y touche jamais.
|
||||
|
||||
Le SDK le parle déjà : passez l’URL dans `client_metadata_url=` quand vous construisez le fournisseur. Lorsque les métadonnées du serveur d’autorisation annoncent `client_id_metadata_document_supported: true`, le fournisseur saute entièrement la requête `/register` : l’URL entre dans le flux en tant que `client_id`, et il n’y a pas de `client_secret`. Lorsque le serveur ne l’annonce pas (la plupart ne le font pas encore), ou que vous ne passez jamais d’URL, le fournisseur se rabat **silencieusement** sur l’enregistrement dynamique, et tout ce qui précède fonctionne exactement comme décrit. Un `client_info` stocké l’emporte toujours sur les deux.
|
||||
|
||||
L’URL doit être en HTTPS avec un chemin autre que la racine ; tout le reste lève une `ValueError` à la construction, avant le moindre échange réseau. L’exemple fourni `examples/clients/simple-auth-client/` la reçoit via la variable d’environnement `MCP_CLIENT_METADATA_URL`.
|
||||
|
||||
## De machine à machine {#machine-to-machine}
|
||||
|
||||
Une tâche nocturne, une étape de CI, un autre service. Il n’y a pas de navigateur et personne pour cliquer sur « autoriser ». C’est le type d’octroi **client credentials** : vous détenez déjà un `client_id` et un `client_secret`, et le point de terminaison de jeton constitue tout le flux.
|
||||
|
||||
`ClientCredentialsOAuthProvider` est le même `httpx2.Auth`, l’humain en moins :
|
||||
|
||||
```python title="client.py" hl_lines="4 27-33"
|
||||
--8<-- "docs_src/oauth_clients/tutorial002.py"
|
||||
```
|
||||
|
||||
Ce qui a changé :
|
||||
|
||||
* Aucun `OAuthClientMetadata`, aucun gestionnaire. Vous passez `client_id` et `client_secret` ; le fournisseur construit autour d’eux un enregistrement `client_credentials` minimal et saute entièrement l’enregistrement dynamique.
|
||||
* `scope` est une chaîne séparée par des espaces, le format qu’OAuth utilise sur la liaison.
|
||||
* Tout ce qui se trouve en aval est identique : le même `TokenStorage`, le même `httpx2.AsyncClient(auth=...)`, le même `streamable_http_client`.
|
||||
|
||||
Par défaut, le secret voyage en authentification HTTP Basic sur la requête de jeton (`client_secret_basic`). Passez `token_endpoint_auth_method="client_secret_post"` pour le placer plutôt dans le corps du formulaire. Certains serveurs d’autorisation n’acceptent que l’une des deux méthodes.
|
||||
|
||||
!!! tip
|
||||
Lisez `client_secret` depuis l’environnement ou un gestionnaire de secrets, jamais depuis le contrôle de version.
|
||||
|
||||
!!! info
|
||||
Un fournisseur de plus se trouve dans `mcp.client.auth.extensions.client_credentials` :
|
||||
**`PrivateKeyJWTOAuthProvider`**, pour les clients qui s’authentifient avec un JWT plutôt qu’avec un
|
||||
secret partagé (`private_key_jwt`, la variante à paire de clés et identité de charge de travail). Il suit
|
||||
le même schéma : construisez-en un, placez-le sur `auth=`. Le même module fournit
|
||||
`SignedJWTParameters` et `static_assertion_provider`, deux utilitaires qui construisent son assertion.
|
||||
|
||||
Il existe une autre situation sans humain : le client appartient à une entreprise dont le fournisseur d’identité, et non l’utilisateur, décide quels serveurs MCP il peut atteindre. C’est un type d’octroi différent, avec son propre modèle de confiance et sa propre page, **[Assertion d’identité](identity-assertion.md)**.
|
||||
|
||||
## En cas d’échec {#when-it-fails}
|
||||
|
||||
Quand le flux OAuth tourne mal, le fournisseur lève une `OAuthFlowError` depuis `mcp.client.auth`. Elle a deux sous-classes. `OAuthRegistrationError` signifie que l’enregistrement n’a pas produit un client utilisable : le serveur d’autorisation a refusé de vous enregistrer, ou il vous a bien enregistré mais avec des identifiants que ce flux ne peut pas utiliser (par exemple une méthode d’authentification qu’il n’implémente pas). `OAuthTokenError` signifie qu’un jeton n’a pas pu être obtenu : le point de terminaison de jeton a dit non, ou une fiche client stockée porte une méthode d’authentification que ce client ne peut pas appliquer, ce qui est signalé pendant la construction de la requête de jeton plutôt qu’envoyé. Un seul `except OAuthFlowError:` couvre la découverte, l’enregistrement, l’autorisation et l’échange.
|
||||
|
||||
Tout n’est pas une erreur de flux. Le réseau peut toujours échouer ; ce sont des exceptions `httpx2` ordinaires et elles passent sans être modifiées.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* `OAuthClientProvider` est un `httpx2.Auth`. Placez-le sur un `httpx2.AsyncClient`, passez celui-ci à `streamable_http_client(url, http_client=...)`, et `Client` ne sait jamais qu’OAuth a eu lieu.
|
||||
* Vous fournissez quatre choses : l’URL du serveur, un `OAuthClientMetadata`, un `TokenStorage` et la paire de gestionnaires redirect/callback.
|
||||
* `TokenStorage` est un `Protocol` : quatre méthodes asynchrones, pas de classe de base. Persistez `client_info` en plus des jetons.
|
||||
* La découverte, l’enregistrement (dynamique, ou via un **Client ID Metadata Document**), PKCE, les vérifications de `state` et `iss`, et l’actualisation des jetons sont l’affaire du fournisseur, pas la vôtre.
|
||||
* `ClientCredentialsOAuthProvider` est la version sans humain : `client_id` + `client_secret`, pas de gestionnaires, pas de navigateur.
|
||||
* Tout échec OAuth est une `OAuthFlowError` ; `OAuthRegistrationError` et `OAuthTokenError` en sont les sous-classes.
|
||||
|
||||
L’autre moitié de cette poignée de main, faire en sorte que votre *serveur* exige le jeton, se trouve dans **[Autorisation](../run/authorization.md)**.
|
||||
87
i18n/fr/pages/client/session-groups.md
Normal file
87
i18n/fr/pages/client/session-groups.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
translation:
|
||||
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73]
|
||||
tool: 1
|
||||
---
|
||||
# Groupes de sessions {#session-groups}
|
||||
|
||||
Un `Client` se connecte à un seul serveur. Les applications réelles en veulent souvent plusieurs (un serveur de recherche, un serveur de base de données, une API interne) et finissent par jongler avec une connexion et une liste d’outils pour chacun.
|
||||
|
||||
**`ClientSessionGroup`** est un objet unique qui détient de nombreuses connexions et fusionne tout ce qu’elles exposent en une seule vue.
|
||||
|
||||
## Deux serveurs {#two-servers}
|
||||
|
||||
Commencez par deux serveurs ordinaires. Ils n’ont rien à voir l’un avec l’autre, si bien que tous deux ont naturellement appelé leur outil `search` :
|
||||
|
||||
```python title="library_server.py" hl_lines="7"
|
||||
--8<-- "docs_src/session_groups/tutorial001.py"
|
||||
```
|
||||
|
||||
```python title="web_server.py" hl_lines="7"
|
||||
--8<-- "docs_src/session_groups/tutorial002.py"
|
||||
```
|
||||
|
||||
## Un groupe {#one-group}
|
||||
|
||||
Créez un `ClientSessionGroup` et appelez **`connect_to_server`** une fois par serveur :
|
||||
|
||||
```python title="client.py" hl_lines="10-12"
|
||||
--8<-- "docs_src/session_groups/tutorial003.py"
|
||||
```
|
||||
|
||||
* `connect_to_server` prend des paramètres de transport, pas un objet serveur : `StdioServerParameters` (depuis `mcp`) pour lancer un sous-processus, ou `StreamableHttpParameters` / `SseServerParameters` (depuis `mcp.client.session_group`) pour un serveur qui écoute déjà sur une URL.
|
||||
* `group.tools` est un `dict[str, Tool]` regroupant les outils de tous les serveurs connectés. `group.resources` et `group.prompts` ont la même forme.
|
||||
* `group.call_tool(name, arguments)` recherche le nom, trouve la session qui le possède et lui transmet l’appel. Vous n’indiquez jamais quel serveur.
|
||||
|
||||
!!! check
|
||||
Placez `client.py` à côté des deux serveurs et exécutez-le. Le second `connect_to_server` refuse :
|
||||
|
||||
```text
|
||||
mcp.shared.exceptions.MCPError: {'search'} already exist in group tools.
|
||||
```
|
||||
|
||||
C’est une `MCPError`, levée avant que quoi que ce soit du second serveur ne soit enregistré. Un nom doit
|
||||
être unique dans **tout** le groupe, et deux serveurs que vous ne contrôlez pas finiront tôt ou tard par entrer en collision.
|
||||
|
||||
## `component_name_hook` {#component_name_hook}
|
||||
|
||||
Vous corrigez cela au niveau du groupe, pas des serveurs. Passez une fonction de `(name, server_info)` et le groupe l’exécute sur chaque nom qu’il enregistre :
|
||||
|
||||
```python title="client.py" hl_lines="7-8 15"
|
||||
--8<-- "docs_src/session_groups/tutorial004.py"
|
||||
```
|
||||
|
||||
Relancez-le. `print(sorted(group.tools))` affiche maintenant les deux :
|
||||
|
||||
```text
|
||||
['Library.search', 'Web.search']
|
||||
```
|
||||
|
||||
* La **clé** est à vous. `by_server` l’a construite à partir de `server_info.name`, le nom avec lequel chaque `MCPServer(...)` a été construit.
|
||||
* Le `Tool` à l’intérieur est intact : `group.tools["Web.search"].name` vaut toujours `"search"`, et c’est ce nom que `call_tool` envoie sur la liaison. Le préfixe ne quitte jamais votre processus.
|
||||
* Cela ne concerne pas que les outils. La ressource `hours` de la bibliothèque est enregistrée sous le nom `Library.hours`.
|
||||
|
||||
!!! tip
|
||||
Le hook s’exécute sur **chaque** nom de **chaque** serveur, pas seulement en cas de conflit : il n’existe pas de
|
||||
mode « préfixe en cas de collision ». Choisissez un schéma et laissez-le s’appliquer partout.
|
||||
|
||||
## Ajouter et retirer des serveurs {#adding-and-removing-servers}
|
||||
|
||||
`connect_to_server` renvoie la `ClientSession` qu’il a ouverte. Conservez-la si vous voulez un jour vous séparer de ce serveur : `await group.disconnect_from_server(session)` retire ses outils, ressources et prompts du groupe.
|
||||
|
||||
Si vous détenez déjà une `ClientSession` connectée (`Client.session` en est une), passez-la à `await group.connect_with_session(server_info, session)` au lieu d’ouvrir un nouveau transport. Elle est agrégée de la même façon. Le groupe ne ferme jamais une session qu’il n’a pas ouverte. `server_info` nomme le serveur pour les préfixes de composants ; sur une connexion de génération 2026, `client.server_info` peut valoir `None` (l’identité est facultative), passez donc votre propre `Implementation(name=..., version=...)` dans ce cas.
|
||||
|
||||
## La poignée de main classique {#the-classic-handshake}
|
||||
|
||||
`ClientSessionGroup` est construit sur `ClientSession`, pas sur `Client`. Chaque `connect_to_server` exécute la poignée de main (handshake) `initialize` classique. Il n’envoie jamais la sonde `server/discover` décrite dans **[Versions du protocole](../protocol-versions.md)**. Tous les serveurs MCP comprennent cette poignée de main, donc cela ne vous coûte aucune compatibilité ; cela signifie seulement qu’un groupe emprunte le chemin plus ancien et plus lent vers un serveur qui pourrait faire mieux.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* `ClientSessionGroup` détient de nombreuses connexions serveur et fusionne leurs outils, ressources et prompts en un `dict` chacun.
|
||||
* `connect_to_server(params)` par serveur. Il prend des paramètres de transport, jamais l’objet serveur ni l’URL que prend un `Client`.
|
||||
* `group.call_tool(name, arguments)` achemine l’appel vers le serveur propriétaire à votre place.
|
||||
* Les noms doivent être uniques dans tout le groupe ; deux serveurs dotés d’un outil `search` ne peuvent pas coexister tels quels.
|
||||
* `component_name_hook=` réécrit chaque nom enregistré. La clé du dict change, pas le nom sur la liaison.
|
||||
* `connect_with_session` ajoute une session que vous détenez déjà ; `disconnect_from_server` en retire une.
|
||||
|
||||
La poignée de main que parle un groupe (et celle, plus rapide, que préfère un `Client`) fait l’objet de **[Versions du protocole](../protocol-versions.md)**.
|
||||
91
i18n/fr/pages/client/subscriptions.md
Normal file
91
i18n/fr/pages/client/subscriptions.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
translation:
|
||||
sections: [8f9558e57f29eee1, a88c587739e0465c, 46ebfd5b325ed041, 4d10b00b57ce4bd9, 2cdb0edd1f59b3e2]
|
||||
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. Un client l’apprend grâce à `client.listen(...)` : une seule requête `subscriptions/listen` dont la réponse *est* le flux. Elle reste ouverte et transporte les notifications de changement que le client a demandées.
|
||||
|
||||
Cette page couvre le côté client : ouvrir le flux, le surveiller en parallèle de votre traitement principal, et gérer la façon dont il se termine. Publier les changements, filtrer et servir la méthode relèvent du serveur ; c’est raconté dans **[Abonnements](../handlers/subscriptions.md)**, sous *Dans votre gestionnaire*. Les exemples de cette page dialoguent avec le serveur de tableau de sprint construit là-bas.
|
||||
|
||||
## Surveiller le flux {#watching-the-stream}
|
||||
|
||||
Un abonnement est un gestionnaire de contexte, un seul. Y entrer envoie la requête, avec vos arguments nommés comme filtre d’abonnement, puis attend la confirmation du serveur : le flux est donc actif au moment où le bloc commence.
|
||||
|
||||
```python title="client.py" hl_lines="15 18 28"
|
||||
--8<-- "docs_src/subscriptions/tutorial003.py"
|
||||
```
|
||||
|
||||
L’itération produit quatre événements typés : `ToolsListChanged`, `PromptsListChanged`, `ResourcesListChanged` et `ResourceUpdated(uri=...)`.
|
||||
|
||||
Un événement dit *ce qui* a changé, jamais *comment*. C’est pourquoi `follow_board` appelle `read_resource` et `list_tools` : l’événement est un signal pour récupérer à nouveau les données. Lisez `event.uri` plutôt que de supposer quelle ressource a bougé : un filtre peut nommer plusieurs URI, et un serveur peut signaler un changement sur une sous-ressource de l’un d’eux.
|
||||
|
||||
Les événements en double qui attendent d’être consommés se fondent en un seul, et une nouvelle récupération vous donne tout de même l’état courant. Seuls les événements identiques se fondent : deux `ResourceUpdated` pour des URI différents sont deux événements.
|
||||
|
||||
Deux autres propriétés de l’objet d’abonnement :
|
||||
|
||||
* `sub.honored` est le filtre que le serveur a confirmé : un `SubscriptionFilter` avec les champs que vous avez passés, lisibles comme attributs (`sub.honored.prompts_list_changed`). `MCPServer` honore tous les types d’événements que vous demandez, il vous renvoie donc votre requête telle quelle. Un serveur qui prend en charge moins de types en confirme moins, et un type confirmé peut malgré tout ne jamais se déclencher. Un serveur peut aussi refuser la requête entière plutôt que de la confirmer (voir [Décider qui peut surveiller](../handlers/subscriptions.md#deciding-who-may-watch) sur la page serveur), ce qui se manifeste comme l’erreur de la requête.
|
||||
* `sub.subscription_id` est l’identifiant de la requête listen, celui qui est apposé sur chaque trame de ce flux. Plusieurs abonnements peuvent être ouverts en même temps, chacun démultiplexé par son propre identifiant.
|
||||
|
||||
## Surveiller sans bloquer {#watching-without-blocking}
|
||||
|
||||
`follow_board` tourne jusqu’à ce que le serveur ferme le flux, ce qui peut ne jamais arriver ; seule, elle monopolise donc votre programme. Les vrais clients veulent l’observateur *à côté* du traitement principal : un agent appelle des outils pendant qu’un observateur maintient à jour un cache ou une interface.
|
||||
|
||||
Ouvrez d’abord l’abonnement, puis démarrez l’observateur et poursuivez votre travail.
|
||||
|
||||
=== "asyncio"
|
||||
|
||||
```python title="app.py" hl_lines="18 20"
|
||||
--8<-- "docs_src/subscriptions/tutorial004_asyncio.py"
|
||||
```
|
||||
|
||||
=== "trio"
|
||||
|
||||
```python title="app.py" hl_lines="18 21"
|
||||
--8<-- "docs_src/subscriptions/tutorial004_trio.py"
|
||||
```
|
||||
|
||||
=== "anyio"
|
||||
|
||||
```python title="app.py" hl_lines="18 21"
|
||||
--8<-- "docs_src/subscriptions/tutorial004_anyio.py"
|
||||
```
|
||||
|
||||
!!! note
|
||||
`app.py` importe `BOARD` et `read_board` depuis le premier exemple, que ce dépôt stocke sous
|
||||
le nom `tutorial003.py`. Si vous enregistrez les fichiers affichés côte à côte sous les noms
|
||||
`client.py` et `app.py`, écrivez plutôt `from client import BOARD, read_board`. L’exemple
|
||||
`watch.py`, plus bas, importe `read_board` de la même façon.
|
||||
|
||||
L’ordre est tout l’enjeu. Rien n’est rejoué : un événement publié avant que votre flux n’existe est perdu. Entrer dans `client.listen(...)` attend la confirmation, si bien que chaque changement à partir de cet instant atteint votre observateur, et l’instantané que vous prenez dans le bloc ne peut en manquer aucun.
|
||||
|
||||
Les requêtes s’exécutent librement à côté d’un flux ouvert, depuis la tâche de l’observateur ou n’importe quelle autre, sur le même client. Comme les événements non consommés *en double* fusionnent, un traitement principal chargé peut produire une seule récupération plutôt que trois. Les événements qui diffèrent ne fusionnent pas : un filtre qui nomme de nombreux URI met en file un événement en attente par URI.
|
||||
|
||||
Pour arrêter de surveiller, sortez du bloc : il n’y a pas d’appel `unsubscribe`. Annuler la tâche qui possède le bloc le fait pour vous, et le SDK annule la requête listen comme le transport l’attend : en Streamable HTTP, en fermant le flux de cette requête. Un observateur qui tourne pendant toute la durée de vie de votre application ne revient jamais de lui-même ; annulez-le donc, ou la portée de son groupe de tâches, à l’arrêt.
|
||||
|
||||
## Les flux se terminent {#streams-end}
|
||||
|
||||
Un flux se termine de l’une de deux façons, toutes deux relevant du flot de contrôle ordinaire. Une fermeture propre côté serveur met fin à la boucle `async for` ; une coupure brutale lève `SubscriptionLost`.
|
||||
|
||||
La différence sert au diagnostic, elle ne change pas ce qu’il faut faire ensuite : le flux a disparu, rien n’a été rejoué, et un observateur toujours intéressé réécoute et récupère à nouveau.
|
||||
|
||||
```python title="watch.py" hl_lines="16 20"
|
||||
--8<-- "docs_src/subscriptions/tutorial005.py"
|
||||
```
|
||||
|
||||
Les serveurs ferment proprement les flux pour des raisons qui leur sont propres, notamment pour se délester d’un abonné dont l’arriéré a trop grossi ; une fin propre n’est donc pas un signal pour cesser de surveiller. Temporisez avant de réécouter.
|
||||
|
||||
`SubscriptionLost` a aussi une cause locale. Le client conserve au plus 1 024 événements non consommés, et un consommateur qui prend autant de retard perd l’abonnement plutôt que de grossir sans limite. Gardez le corps de la boucle `async for` court et faites le travail lent ailleurs.
|
||||
|
||||
`keep_following` n’intercepte que `SubscriptionLost`. Entrer dans `listen()` peut aussi lever `MCPError` (la connexion a échoué, ou le serveur ne sert pas la méthode), `TimeoutError` (aucune confirmation n’est arrivée) et `ListenNotSupportedError` (une connexion antérieure à 2026). Décidez lesquelles votre observateur devrait retenter : la dernière ne se résorbe jamais.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* Entrez dans `async with client.listen(...)` ; l’entrée attend la confirmation, donc rien de ce qui est publié ensuite n’est manqué.
|
||||
* Itérez avec `async for event in sub`. Les événements sont des signaux pour récupérer à nouveau, jamais des charges utiles.
|
||||
* Ouvrez l’abonnement, puis lancez l’observateur comme tâche, et les appels d’outils continuent de circuler à côté.
|
||||
* Une fin propre arrête la boucle ; une coupure lève `SubscriptionLost`. Dans les deux cas : réécoutez, récupérez à nouveau, en temporisant d’abord.
|
||||
* Sortir du bloc, c’est se désabonner.
|
||||
|
||||
Publier ces événements, restreindre le filtre et passer à l’échelle au-delà d’un seul processus relèvent du serveur : **[Abonnements](../handlers/subscriptions.md)**. Ces mêmes événements maintiennent aussi à jour un cache côté client, et **[Mise en cache](caching.md)** est la page suivante.
|
||||
128
i18n/fr/pages/client/transports.md
Normal file
128
i18n/fr/pages/client/transports.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
translation:
|
||||
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, ff7401df479af877, 3d0832f39b0d7059, d4bf7e4479637768, 05e20c0a798860e7]
|
||||
tool: 1
|
||||
---
|
||||
# Transports côté client {#client-transports}
|
||||
|
||||
Chaque `Client` dialogue avec son serveur via un **transport** : ce qui achemine réellement les messages.
|
||||
|
||||
Vous n’en configurez jamais un séparément. `Client` prend un seul argument positionnel et déduit le transport de son type.
|
||||
|
||||
Le côté *serveur* de chacun (ce que fait `mcp.run()` et ce que vous déployez) est traité dans **[Exécuter votre serveur](../run/index.md)**.
|
||||
|
||||
## En mémoire {#in-memory}
|
||||
|
||||
Passez l’objet serveur lui-même :
|
||||
|
||||
```python title="client.py" hl_lines="14"
|
||||
--8<-- "docs_src/client_transports/tutorial001.py"
|
||||
```
|
||||
|
||||
Pas de sous-processus, pas de port, aucun octet sur une liaison. Le client et le serveur sont deux objets dans le même processus, et l’appel passe tout de même par la véritable couche protocolaire : `search_books` est listé, validé et invoqué exactement comme il le serait via HTTP.
|
||||
|
||||
Cela en fait deux choses à la fois :
|
||||
|
||||
* **Un banc de test.** Chaque exemple de cette documentation est exécuté de cette façon, et la page **[Tests](../get-started/testing.md)** construit tout son modèle autour de lui.
|
||||
* **Une API d’intégration.** Une application qui construit le serveur n’a pas besoin d’un saut réseau pour appeler ses outils.
|
||||
|
||||
## Streamable HTTP {#streamable-http}
|
||||
|
||||
Passez une URL sous forme de chaîne et vous obtenez **Streamable HTTP**, le transport derrière lequel vous déployez :
|
||||
|
||||
```python title="client.py" hl_lines="5"
|
||||
--8<-- "docs_src/client_transports/tutorial002.py"
|
||||
```
|
||||
|
||||
C’est tout le client de production. `Client` enveloppe l’URL dans `streamable_http_client(...)` pour vous, par-dessus un `httpx2.AsyncClient` configuré comme MCP l’exige : `follow_redirects=True`, un délai d’expiration de 30 secondes pour connect/write/pool, et un délai de lecture de 300 secondes parce que le serveur peut garder un flux de réponse ouvert.
|
||||
|
||||
!!! check
|
||||
Un `Client` que vous venez de construire n’est **pas** connecté. La construction ne fait que choisir le transport ;
|
||||
c’est `async with` qui l’ouvre. Tentez d’accéder à la connexion avant d’y entrer et le SDK vous le signale :
|
||||
|
||||
```text
|
||||
RuntimeError: Client must be used within an async context manager
|
||||
```
|
||||
|
||||
Rien n’a été résolu, récupéré ni lancé quand vous avez écrit `Client("http://...")`. Cette ligne ne coûte rien.
|
||||
|
||||
### Fournir votre propre `httpx2.AsyncClient` {#bring-your-own-httpx2asyncclient}
|
||||
|
||||
Dès que vous avez besoin d’un en-tête `Authorization`, d’un cookie, d’un proxy, de mTLS ou d’un délai d’expiration différent, construisez le `httpx2.AsyncClient` vous-même et passez-le à `streamable_http_client` :
|
||||
|
||||
```python title="client.py" hl_lines="8-14"
|
||||
--8<-- "docs_src/client_transports/tutorial003.py"
|
||||
```
|
||||
|
||||
Deux points à remarquer :
|
||||
|
||||
* Le `httpx2.AsyncClient` vous appartient, donc c’est **vous** qui y entrez et en sortez. Le SDK ne ferme jamais un client qu’il n’a pas créé.
|
||||
* `streamable_http_client(url, http_client=...)` renvoie un transport, et `Client(transport)` l’accepte comme n’importe quoi d’autre.
|
||||
|
||||
Une remarque sur TLS : `httpx2` vérifie les certificats par rapport au magasin de confiance du système d’exploitation (via
|
||||
[`truststore`](https://pypi.org/project/truststore/)), et non par rapport à une liste d’autorités de certification embarquée. Dans un environnement sans
|
||||
magasin d’autorités de certification système utilisable (certains conteneurs minimaux), définissez les variables d’environnement standard `SSL_CERT_FILE`/`SSL_CERT_DIR`
|
||||
ou passez un `verify=ssl_context` explicite à votre `httpx2.AsyncClient`
|
||||
(le contexte se trouve dans
|
||||
[`httpx` et `httpx-sse` remplacés par `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)).
|
||||
|
||||
!!! warning
|
||||
`streamable_http_client` acceptait autrefois `headers=` et `timeout=` directement. Ce n’est plus le cas :
|
||||
ses seuls paramètres sont `url`, `http_client` et `terminate_on_close`. Utilisez `headers=` par
|
||||
habitude et vous obtenez :
|
||||
|
||||
```text
|
||||
TypeError: streamable_http_client() got an unexpected keyword argument 'headers'
|
||||
```
|
||||
|
||||
Tout ce qui relève de HTTP se trouve désormais sur l’unique `httpx2.AsyncClient` que vous passez.
|
||||
|
||||
!!! info
|
||||
`httpx2` conserve l’API familière de `httpx` ; si vous connaissez `httpx`, vous savez déjà comment gérer ici l’authentification,
|
||||
les proxys, les hooks d’événements, les nouvelles tentatives et les limites de connexions. Le SDK n’ajoute rien par-dessus et ne retire
|
||||
rien. C’est aussi là qu’OAuth se branche :
|
||||
`httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Tout ce flux est décrit dans **[Clients OAuth](oauth-clients.md)**.
|
||||
|
||||
## stdio {#stdio}
|
||||
|
||||
Un serveur **stdio** est un sous-processus. Le client le lance, écrit du JSON-RPC sur son stdin et lit du JSON-RPC depuis son stdout. C’est ainsi qu’un hôte de bureau exécute un serveur sur votre machine : un hôte *est* ce code plus une interface utilisateur, et **[Se connecter à un véritable hôte](../get-started/real-host.md)** montre la même relation vue du côté de l’hôte, sous forme de fichier de configuration.
|
||||
|
||||
Décrivez le processus avec `StdioServerParameters`, transformez-le en transport avec `stdio_client`, et passez *cela* à `Client` :
|
||||
|
||||
```python title="client.py" hl_lines="4-8 12"
|
||||
--8<-- "docs_src/client_transports/tutorial004.py"
|
||||
```
|
||||
|
||||
`Client` n’accepte pas l’objet de paramètres seul. `StdioServerParameters` est de la configuration ; `stdio_client(server)` est le transport qui sait lancer un processus à partir de celle-ci. Enveloppez toujours.
|
||||
|
||||
Quitter le bloc `async with` arrête aussi le sous-processus : fermeture de stdin, attente, arrêt forcé s’il traîne. Vous ne le nettoyez jamais vous-même.
|
||||
|
||||
!!! warning
|
||||
Le processus enfant n’hérite **pas** de votre environnement. Il reçoit une liste d’autorisation minimale (`HOME`, `LOGNAME`,
|
||||
`PATH`, `SHELL`, `TERM` et `USER` sous POSIX), de sorte que rien de sensible ne fuite vers un processus que vous n’avez peut-être
|
||||
pas écrit.
|
||||
|
||||
Un serveur qui a besoin d’une clé d’API ne l’y trouvera pas. Passez-la explicitement avec `env=` ; ces
|
||||
variables sont fusionnées par-dessus la liste d’autorisation. C’est ce que fait `BOOKSHOP_API_KEY` ci-dessus.
|
||||
|
||||
## SSE {#sse}
|
||||
|
||||
`sse_client(url)`, du module `mcp.client.sse`, est le transport HTTP que Streamable HTTP a remplacé. Enveloppez-le de la même manière, `Client(sse_client("http://localhost:8000/sse"))`, pour dialoguer avec un serveur qui le parle encore, et ne construisez rien de nouveau dessus.
|
||||
|
||||
## Le protocole `Transport` {#the-transport-protocol}
|
||||
|
||||
Pour `Client`, tout ce qui précède est une seule et même chose.
|
||||
|
||||
Un **transport** est n’importe quel gestionnaire de contexte asynchrone qui produit une paire `(read, write)` de flux de messages : formellement, le protocole `Transport` de `mcp.client`. `Client` résout son argument selon son type : un objet serveur se connecte dans le processus, une `str` devient `streamable_http_client(url)`, et tout le reste est ouvert directement comme transport. C’est cette dernière règle qui explique pourquoi `stdio_client(...)`, `streamable_http_client(...)` et `sse_client(...)` s’insèrent tous au même emplacement, et pourquoi vous pouvez écrire le vôtre.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* `Client(mcp)` (l’objet serveur) se connecte en mémoire. Utilisez-le pour les tests et pour l’intégration.
|
||||
* `Client("http://.../mcp")` (une URL) se connecte via Streamable HTTP, le transport de production.
|
||||
* Les en-têtes, l’authentification, les proxys et les délais d’expiration vont sur un `httpx2.AsyncClient` que vous passez à `streamable_http_client(url, http_client=...)`. Il n’y a pas de mot-clé `headers=`.
|
||||
* stdio s’écrit `Client(stdio_client(StdioServerParameters(...)))`, jamais l’objet de paramètres seul.
|
||||
* Le sous-processus reçoit un environnement sous liste d’autorisation, pas le vôtre ; `env=` s’y ajoute.
|
||||
* Un transport est tout ce sur quoi vous pouvez faire `async with x as (read, write)`. `Client` transmet directement à ce protocole tout ce qui n’est ni un objet serveur ni une URL.
|
||||
* Construire un `Client` choisit le transport. `async with` l’ouvre.
|
||||
|
||||
Une fois le transport ouvert, les deux côtés doivent s’accorder sur une version du protocole. En temps normal, vous n’y pensez jamais ; le jour où vous devez y penser, la page à consulter est **[Versions du protocole](../protocol-versions.md)**.
|
||||
97
i18n/fr/pages/deprecated.md
Normal file
97
i18n/fr/pages/deprecated.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
---
|
||||
translation:
|
||||
sections: [20541a40dbdd5980, 01262a123ad9501d, 429db5b574a2ac08, 56b2d49da412cb28, 6a1717123fe4513c]
|
||||
tool: 1
|
||||
---
|
||||
# Fonctionnalités obsolètes {#deprecated-features}
|
||||
|
||||
La spécification 2026-07-28 retire cinq éléments. Le SDK les implémente toujours tous, et chacun d’eux porte désormais un **avertissement d’obsolescence**.
|
||||
|
||||
Le tableau ci-dessous nomme chaque fonctionnalité obsolète, la raison de sa disparition et le remplacement sur lequel vous appuyer.
|
||||
|
||||
## Ce qui est obsolète {#what-is-deprecated}
|
||||
|
||||
| Obsolète | Pourquoi | Ce que vous faites à la place |
|
||||
|---|---|---|
|
||||
| **Racines (roots)** : `ctx.session.list_roots()`, `client.send_roots_list_changed()`, le `list_roots_callback=` que vous passez à `Client(...)` | La [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) retire la capacité. | Prenez les chemins comme arguments d’outil ordinaires ou comme URI de ressource, ou intégrez une `ListRootsRequest` dans un `InputRequiredResult` (voir **[Requêtes à plusieurs allers-retours (multi-round-trip)](handlers/multi-round-trip.md)**). |
|
||||
| **Échantillonnage (sampling) à l’initiative du serveur** : `ctx.session.create_message()`, le `sampling_callback=` que vous passez à `Client(...)` | La SEP-2577 retire la capacité. | Renvoyez `InputRequiredResult` et laissez le client réessayer l’appel (voir **[Requêtes à plusieurs allers-retours](handlers/multi-round-trip.md)**). |
|
||||
| **Journalisation par le protocole** : `ctx.log()`, `ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`, `ctx.session.send_log_message()`, `client.set_logging_level()` | La SEP-2577 retire la capacité. Rien dans le protocole ne la remplace. | Un `import logging` ordinaire vers stderr (voir **[Journalisation](handlers/logging.md)**). |
|
||||
| **`ping`** : `client.send_ping()` | **Supprimé** du protocole, pas simplement obsolète. Il n’y a pas de méthode `ping` en version 2026-07-28. | Rien. Cela ne fonctionne que sur une connexion `mode="legacy"`. |
|
||||
| **Progression client->serveur** : `client.send_progress_notification()` | La version 2026-07-28 réserve la progression au sens serveur->client. | Rien à envoyer. Votre *serveur* signale sa progression avec `ctx.report_progress()` (voir **[Progression](handlers/progress.md)**). |
|
||||
|
||||
Trois choses ressortent de ce tableau :
|
||||
|
||||
* Les racines, l’échantillonnage et la journalisation vont ensemble. Une seule proposition, la **SEP-2577**, rend les trois capacités obsolètes d’un coup.
|
||||
* L’échantillonnage et les racines partagent un problème plus profond : ce sont des endroits où un **serveur** envoie une **requête** au **client**. C’est toute cette direction que la version 2026-07-28 remplace par les **[Requêtes à plusieurs allers-retours](handlers/multi-round-trip.md)**. Ce sont les méthodes RPC autonomes (`sampling/createMessage`, `roots/list` et `elicitation/create` en mode push) qui disparaissent ; les types de charge utile `CreateMessageRequest` / `ListRootsRequest` / `ElicitRequest` survivent, intégrés dans `InputRequiredResult.input_requests`, et côté client ils aboutissent aux mêmes fonctions de rappel (callbacks).
|
||||
* `ping` est l’exception. Le protocole ne le rend pas obsolète, il le supprime. La méthode du SDK avertit quand même (son message dit *removed*, pas *deprecated*) et l’appeler sur une connexion moderne répond par *« Method not found »*.
|
||||
|
||||
## L’obsolescence est indicative {#deprecated-is-advisory}
|
||||
|
||||
Rien ne casse aujourd’hui.
|
||||
|
||||
Chaque méthode ci-dessus continue de fonctionner sur toute session qui a négocié la version **2025-11-25 ou antérieure**. Fixez `mode="legacy"` sur le client et vous obtenez exactement le comportement d’avant 2026. Il n’y a aucun changement sur la liaison et la négociation des capacités est inchangée.
|
||||
|
||||
Ce qui change, c’est que vous obtenez un avertissement visible la première fois que chacune s’exécute :
|
||||
|
||||
```text
|
||||
MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
|
||||
```
|
||||
|
||||
`MCPDeprecationWarning` hérite de `UserWarning`, **pas** de `DeprecationWarning`. C’est délibéré : le filtre par défaut de Python n’affiche `DeprecationWarning` que dans le code exécuté directement en tant que `__main__`, ce qui explique que les bibliothèques rendent des choses obsolètes sans que personne ne le remarque pendant deux ans. Celui-ci apparaît partout, sans option `-W`.
|
||||
|
||||
!!! warning
|
||||
« Indicatif » s’arrête à la liaison. L’échantillonnage et les racines sont des *requêtes*
|
||||
du serveur vers le client, et une session 2026-07-28 n’a aucun canal pour en transporter
|
||||
une. Appelez `ctx.session.create_message()` dans un outil sur une connexion moderne :
|
||||
l’avertissement se déclenche quand même, puis l’envoi échoue avec une erreur :
|
||||
|
||||
```text
|
||||
Cannot send 'sampling/createMessage': this transport context has no back-channel
|
||||
for server-initiated requests.
|
||||
```
|
||||
|
||||
Deux signaux, dans cet ordre. Le `MCPDeprecationWarning` se déclenche dès que vous
|
||||
appelez la méthode, sur n’importe quelle connexion. L’erreur est ce qui revient quand le
|
||||
SDK tente ensuite l’envoi. Ces deux fonctionnalités ne marchent de bout en bout que sur
|
||||
une connexion `mode="legacy"` dont le client a enregistré la fonction de rappel
|
||||
correspondante.
|
||||
|
||||
## Faire taire l’avertissement {#silencing-the-warning}
|
||||
|
||||
Dans du nouveau code, ne le faites pas.
|
||||
|
||||
Mais un serveur que vous maintenez et qui sert réellement des clients d’avant 2026 a parfaitement droit à un journal silencieux. Filtrez la catégorie avant l’exécution du premier appel obsolète :
|
||||
|
||||
```python
|
||||
import warnings
|
||||
|
||||
from mcp import MCPDeprecationWarning
|
||||
|
||||
warnings.filterwarnings("ignore", category=MCPDeprecationWarning)
|
||||
```
|
||||
|
||||
C’est toute l’API. Il n’y a pas d’interrupteur par méthode, et vous n’en voulez pas : l’intérêt d’une catégorie unique, c’est qu’une ligne la fait taire et qu’une ligne la rétablit.
|
||||
|
||||
!!! check
|
||||
Inversez le filtre et vous obtenez gratuitement un test de non-régression. Ajoutez
|
||||
`"error::mcp.MCPDeprecationWarning"` au réglage `filterwarnings` de votre configuration
|
||||
pytest et l’appel obsolète **lève une exception** au lieu d’avertir. Un outil nommé
|
||||
`old_log` qui appelle encore `ctx.info()` cesse de passer et se met à signaler :
|
||||
|
||||
```text
|
||||
Error executing tool old_log: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
|
||||
```
|
||||
|
||||
Une ligne de configuration pytest, et un appel obsolète ne peut plus jamais se glisser
|
||||
de nouveau dans votre base de code sans faire échouer un test.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* La spécification 2026-07-28 rend obsolètes les **racines**, l’**échantillonnage** à l’initiative du serveur et la **journalisation** par le protocole (toutes via la [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577)), restreint la **progression** au sens serveur vers client et supprime **`ping`**.
|
||||
* La colonne des remplacements vous oriente : **[Requêtes à plusieurs allers-retours](handlers/multi-round-trip.md)** pour l’échantillonnage et les racines, **[Journalisation](handlers/logging.md)** pour la journalisation, **[Progression](handlers/progress.md)** pour la progression. `ping` n’a besoin de rien du tout.
|
||||
* L’obsolescence est indicative : aucun changement sur la liaison, tout continue de fonctionner sur les sessions d’avant 2026, et vous obtenez un `MCPDeprecationWarning` visible (un `UserWarning`, donc actif par défaut).
|
||||
* L’échantillonnage et les racines ont en plus besoin d’un canal de retour (back-channel) qu’une session 2026-07-28 n’a pas. Sur une connexion moderne, ils avertissent puis lèvent une exception.
|
||||
* `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` fait taire toute la catégorie ; `"error::mcp.MCPDeprecationWarning"` dans pytest la transforme en échec de test.
|
||||
* Aucun nouveau code ne devrait s’appuyer sur l’une de ces fonctionnalités.
|
||||
|
||||
Toutes les autres pages de cette documentation enseignent l’API actuelle.
|
||||
144
i18n/fr/pages/get-started/first-steps.md
Normal file
144
i18n/fr/pages/get-started/first-steps.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686]
|
||||
tool: 1
|
||||
---
|
||||
# Premiers pas {#first-steps}
|
||||
|
||||
La **[page d’accueil](../index.md)** va vite : écrire un serveur, l’exécuter, appeler un outil.
|
||||
|
||||
Cette page prend son temps, avec les trois choses qu’un serveur peut exposer, et un nom pour chaque notion rencontrée en chemin.
|
||||
|
||||
## Hôte, client et serveur {#host-client-and-server}
|
||||
|
||||
Trois mots que vous verrez sur chaque page à partir d’ici :
|
||||
|
||||
* Un **hôte** est l’application LLM : Claude, un IDE, un environnement d’exécution d’agents. C’est ce à quoi l’utilisateur parle.
|
||||
* Un **client** vit à l’intérieur de l’hôte et parle MCP. L’hôte exécute un client par serveur auquel il est connecté.
|
||||
* Un **serveur** est ce que vous construisez avec ce SDK. Il expose des choses aux clients. Il ne parle jamais directement au modèle.
|
||||
|
||||
Vous écrivez le serveur. Les hôtes sont le produit de quelqu’un d’autre. Le SDK vous fournit aussi un `Client`. Vous l’utiliserez pour tester vos serveurs, et il apparaît plus loin sur cette page.
|
||||
|
||||
## Les trois primitives {#the-three-primitives}
|
||||
|
||||
Un serveur expose exactement trois sortes de choses. Ce qui les distingue, c’est **qui décide de les utiliser** :
|
||||
|
||||
| Primitive | Contrôlée par | Ce que c’est | Exemple |
|
||||
|----------------|-----------------|----------------------------------------------------------------------|------------------------------------------------|
|
||||
| **Outils** | Le modèle | Une fonction que le modèle appelle pour agir | Un appel d’API, une écriture en base de données |
|
||||
| **Ressources** | L’application | Des données que l’hôte charge dans le contexte du modèle | Le contenu d’un fichier, une réponse d’API |
|
||||
| **Prompts** | L’utilisateur | Un modèle de message réutilisable que l’utilisateur invoque par son nom | Une commande slash, une entrée de menu |
|
||||
|
||||
« Contrôlée par » est tout l’intérêt de la distinction. Un outil s’exécute parce que le **modèle** a décidé de l’appeler. Une ressource est jointe parce que l’**application** a décidé que le modèle en avait besoin. Un prompt s’exécute parce que l’**utilisateur** l’a choisi.
|
||||
|
||||
!!! info
|
||||
Si vous avez déjà construit une API web, vous avez l’essentiel de l’intuition : une **ressource** est un `GET`
|
||||
(elle charge des données et ne modifie rien) et un **outil** est un `POST` (il effectue un travail et peut avoir
|
||||
des effets de bord). Un **prompt** n’a pas d’équivalent HTTP ; il se rapproche d’une requête enregistrée que
|
||||
l’utilisateur exécute par son nom.
|
||||
|
||||
## Un serveur, les trois à la fois {#one-server-all-three}
|
||||
|
||||
```python title="server.py" hl_lines="6 12 18"
|
||||
--8<-- "docs_src/first_steps/tutorial001.py"
|
||||
```
|
||||
|
||||
Trois fonctions ordinaires, trois décorateurs. Chaque décorateur constitue à lui seul tout l’enregistrement :
|
||||
|
||||
* `@mcp.tool()` fait de `add` un **outil**.
|
||||
* `@mcp.resource("greeting://{name}")` fait de `greeting` un **modèle de ressource** (resource template) : le `{name}` dans l’URI est le paramètre de la fonction.
|
||||
* `@mcp.prompt()` fait de `summarize` un **prompt**. La chaîne qu’il renvoie devient un message utilisateur.
|
||||
|
||||
Tout le reste (le nom, la description, le schéma des arguments), le SDK le lit dans la fonction elle-même : son nom, sa docstring, ses annotations de type. Vous n’avez rien déclaré de tout cela séparément.
|
||||
|
||||
!!! tip
|
||||
Les deux moitiés du SDK ont deux chemins d’import : `from mcp import Client` et
|
||||
`from mcp.server import MCPServer`. Il n’existe pas de `from mcp import MCPServer`.
|
||||
|
||||
### Essayer {#try-it}
|
||||
|
||||
Lancez-le avec le MCP Inspector :
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Ouvrez l’URL qu’il affiche. L’Inspector a un onglet par primitive ; parcourez-les dans l’ordre.
|
||||
|
||||
**Tools.** Une entrée : `add`, décrite comme *Add two numbers.* Le formulaire comporte un champ entier obligatoire pour `a` et un autre pour `b`. Remplissez-les, lancez l’appel, et le résultat est `3`. L’Inspector a construit ce formulaire à partir de `a: int, b: int`. Tous les autres clients font de même.
|
||||
|
||||
**Resources.** La liste *Resources* est vide. `greeting` se trouve sous **Resource Templates**, parce que `greeting://{name}` a un paramètre : il n’y a aucune ressource unique à lister tant que personne n’a fourni de `name`. Donnez-lui `World` et lisez-la :
|
||||
|
||||
```text
|
||||
Hello, World!
|
||||
```
|
||||
|
||||
**Prompts.** Une entrée : `summarize`, avec un seul argument obligatoire, `text`. Récupérez-le avec un peu de texte et vous recevez un message avec `role: user` et votre chaîne rendue comme contenu. Un prompt n’est rien d’autre que cela : une fonction qui construit des messages.
|
||||
|
||||
L’Inspector a exécuté votre serveur via **stdio**, l’un des transports qu’un serveur MCP peut parler. Vous n’en choisissez pas encore un ; **[Exécuter votre serveur](../run/index.md)** est la page consacrée à ce sujet.
|
||||
|
||||
## Capacités {#capabilities}
|
||||
|
||||
Vous avez vu trois onglets dans l’Inspector. Comment savait-il qu’il y en avait trois ?
|
||||
|
||||
Lorsqu’un client se connecte, le serveur déclare ses **capacités** (capabilities) : les familles de requêtes auxquelles il répondra. Le client utilise cette déclaration pour décider de ce qu’il peut même demander. Vous ne l’avez jamais écrite ; `MCPServer` la déclare pour vous.
|
||||
|
||||
Regardez par vous-même. Le `Client` du SDK accepte directement l’objet serveur et s’y connecte **en mémoire** (ni sous-processus, ni port) :
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(mcp) as client:
|
||||
print(client.server_capabilities.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```text
|
||||
{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}}
|
||||
```
|
||||
|
||||
Ce dictionnaire, ce sont les **capacités** déclarées de votre serveur. C’est la première chose qu’apprend chaque client qui se connecte :
|
||||
|
||||
| Capacité | Le client peut désormais appeler |
|
||||
|-------------|---------------------------------------------------------------|
|
||||
| `tools` | `tools/list`, `tools/call` |
|
||||
| `resources` | `resources/list`, `resources/templates/list`, `resources/read` |
|
||||
| `prompts` | `prompts/list`, `prompts/get` |
|
||||
|
||||
`MCPServer` sert les trois primitives, donc les trois sont toujours déclarées.
|
||||
|
||||
Remarquez ce qui n’y figure pas. `completions` (la complétion automatique des arguments pour les modèles de ressources et les prompts) nécessite un gestionnaire que vous écrivez ; ce serveur n’en a pas, donc la capacité est absente et un client bien élevé ne demandera rien. C’est la règle pour tout ce qui est facultatif : enregistrez la chose et la capacité apparaît ; **[Complétions](../servers/completions.md)** le prouve.
|
||||
|
||||
!!! info
|
||||
`Client(mcp)` est le même client en mémoire avec lequel chaque exemple de cette documentation est testé, et
|
||||
c’est ainsi que vous testerez les vôtres. Il a droit à une page entière : **[Tester](testing.md)**.
|
||||
|
||||
## Ce que vous n’avez pas écrit {#what-you-did-not-write}
|
||||
|
||||
Reprenez cette page depuis le début. Vous avez écrit trois petites fonctions Python. Vous n’avez **pas** écrit :
|
||||
|
||||
* De JSON Schema. `a: int, b: int` *est* le schéma de `add`.
|
||||
* De gestionnaire de requêtes. `tools/list`, `resources/read`, `prompts/get` : tous servis pour vous.
|
||||
* De déclaration de capacités. `MCPServer` l’a faite pour vous.
|
||||
* Une seule ligne de protocole. La négociation de version, l’encapsulation JSON-RPC, l’échange de capacités : tout cela s’est passé à l’intérieur de `mcp dev` et de `Client(mcp)`, et vous n’en avez rien vu.
|
||||
|
||||
Ce rapport est tout l’intérêt du SDK.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* Un **hôte** est l’application LLM, un **client** est sa moitié qui parle MCP, un **serveur** est ce que vous construisez.
|
||||
* Les outils sont contrôlés par le **modèle**, les ressources par l’**application**, les prompts par l’**utilisateur**.
|
||||
* Un décorateur par primitive : `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Le nom, la description et le schéma viennent de la fonction.
|
||||
* Un URI avec un `{param}` crée un **modèle** de ressource, listé séparément des ressources concrètes.
|
||||
* Les **capacités** du serveur sont déclarées pour vous, et un client ne demande que ce qu’un serveur déclare.
|
||||
* `Client(mcp)` se connecte à l’objet serveur en mémoire : votre banc d’essai dès le premier jour.
|
||||
|
||||
La suite, c’est **[Se connecter à un vrai hôte](real-host.md)** : ce serveur dans Claude Desktop ou un IDE, pour de vrai. Puis **[Tester](testing.md)** : une page, un client en mémoire, et vous n’aurez plus jamais à deviner si cela fonctionne. Ensuite, chaque primitive a droit à sa propre page, en commençant par celle que pilote le modèle : **[Outils](../servers/tools.md)**.
|
||||
57
i18n/fr/pages/get-started/index.md
Normal file
57
i18n/fr/pages/get-started/index.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ed4a756b4c53c585, 97e2fb315b7fe398, 4d04f1c6f4bf6c1d, 577d73078fc62baf]
|
||||
tool: 1
|
||||
---
|
||||
# Prise en main {#get-started}
|
||||
|
||||
Vous débutez avec MCP, ou avec ce SDK ? Commencez ici. Ces pages vous mènent de zéro à un
|
||||
serveur fonctionnel et testé : [installez le SDK](installation.md), construisez votre
|
||||
[premier serveur](first-steps.md), [connectez-le à un hôte réel](real-host.md) et
|
||||
[testez-le](testing.md) avec un client en mémoire.
|
||||
|
||||
## Exécuter le code {#run-the-code}
|
||||
|
||||
Tous les blocs de code peuvent être copiés et utilisés tels quels : ce sont des fichiers complets et fonctionnels.
|
||||
|
||||
Pour suivre, collez un bloc dans un fichier `server.py` et ouvrez-le dans le MCP Inspector :
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Il est **FORTEMENT recommandé** d’écrire (ou de copier) le code, de le modifier et de l’exécuter localement. C’est en l’utilisant dans votre propre éditeur que vous en saisirez vraiment l’intérêt : le peu de code à écrire, l’autocomplétion, les vérifications de type qui détectent les erreurs avant même que vous n’exécutiez quoi que ce soit.
|
||||
|
||||
## Vous n’aurez pas à deviner {#you-will-not-be-guessing}
|
||||
|
||||
Chaque exemple de cette documentation est un fichier complet sous [`docs_src/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/docs_src) dans le dépôt du SDK lui-même, et chacun d’eux est exécuté par la suite de tests du SDK via un **client en mémoire** :
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from mcp import Client
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_add() -> None:
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("add", {"a": 1, "b": 2})
|
||||
assert result.structured_content == {"result": 3}
|
||||
```
|
||||
|
||||
Aucun sous-processus, aucun port, aucun transport. `Client(mcp)` se connecte directement à l’objet serveur.
|
||||
|
||||
Si une modification du SDK casse un exemple de l’une de ces pages, la CI passe au rouge avant la page. Le code que vous lisez ici est le code qui s’exécute.
|
||||
|
||||
Vous l’utiliserez vous-même dans [Tester](testing.md) ; c’est aussi ainsi que vous testez vos propres serveurs.
|
||||
|
||||
## Où aller ensuite {#where-to-go-next}
|
||||
|
||||
Une fois qu’un serveur tourne, le reste de cette documentation est une référence, pas un cours.
|
||||
Chaque page se suffit à elle-même, alors allez directement à ce dont vous avez besoin :
|
||||
|
||||
* Ce qu’un serveur expose (outils, ressources, prompts), c’est **[Serveurs](../servers/index.md)**.
|
||||
* Ce qui est disponible dans les fonctions que vous enregistrez, c’est **[Dans votre gestionnaire](../handlers/index.md)**.
|
||||
* Le mettre à disposition des clients (stdio, HTTP, votre application FastAPI existante), c’est **[Exécuter votre serveur](../run/index.md)**.
|
||||
* Construire l’autre côté, une application qui *utilise* des serveurs MCP, c’est **[Clients](../client/index.md)**.
|
||||
47
i18n/fr/pages/get-started/installation.md
Normal file
47
i18n/fr/pages/get-started/installation.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
translation:
|
||||
sections: [6e2f9bab94d5ed36, 8cf653388f69e28b, 6fd9ea2f65de0df6]
|
||||
tool: 1
|
||||
---
|
||||
# Installation {#installation}
|
||||
|
||||
Le SDK Python est disponible sur PyPI sous le nom [`mcp`](https://pypi.org/project/mcp/). Il nécessite **Python 3.10+**.
|
||||
|
||||
Cette documentation décrit la **v2**, la ligne de versions stable actuelle :
|
||||
|
||||
=== "uv"
|
||||
|
||||
```bash
|
||||
uv add "mcp[cli]"
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
|
||||
```bash
|
||||
pip install "mcp[cli]"
|
||||
```
|
||||
|
||||
!!! note "Vous venez de la v1 ?"
|
||||
La v2 est une version majeure avec des changements incompatibles ; le **[Guide de migration](../migration.md)**
|
||||
les couvre tous. Si votre *paquet* dépend de `mcp` et n’est pas prêt à migrer, conservez une
|
||||
borne supérieure `<2` (par exemple `mcp>=1.28,<2`) pour qu’une résolution sans version épinglée reste sur la ligne 1.x.
|
||||
|
||||
## Ce qui est installé {#what-gets-installed}
|
||||
|
||||
Vous n’avez pas besoin de connaître tout cela pour utiliser le SDK, mais si vous vous demandez à quoi sert chaque dépendance :
|
||||
|
||||
* `mcp-types` : tous les types du protocole (requêtes, résultats, blocs de contenu) dans un paquet à part, versionné au même rythme que le SDK. Le code qui dépend de `mcp` l’importe via l’alias `mcp.types` (tous les `from mcp.types import ...` de cette documentation) ; n’importez `mcp_types` directement que dans un projet qui installe `mcp-types` sans le SDK.
|
||||
* [`anyio`](https://anyio.readthedocs.io/) : le runtime asynchrone. Tout le SDK est écrit au-dessus d’anyio, il fonctionne donc aussi bien avec `asyncio` qu’avec `trio`.
|
||||
* [`pydantic`](https://docs.pydantic.dev/) : la base de tous les modèles `mcp.types`, ainsi que toute la génération et la validation de schémas.
|
||||
* [`httpx2`](https://pypi.org/project/httpx2/) : le client HTTP derrière les transports *client* Streamable HTTP et SSE, avec prise en charge intégrée des server-sent events.
|
||||
* [`starlette`](https://www.starlette.io/), [`uvicorn`](https://www.uvicorn.org/), [`sse-starlette`](https://pypi.org/project/sse-starlette/) et [`python-multipart`](https://pypi.org/project/python-multipart/) : les transports HTTP *serveur*.
|
||||
* [`jsonschema`](https://pypi.org/project/jsonschema/) : valide la sortie structurée d’un outil par rapport au schéma de sortie qu’il déclare.
|
||||
* [`pyjwt[crypto]`](https://pyjwt.readthedocs.io/) : gestion des jetons OAuth pour l’autorisation.
|
||||
* [`opentelemetry-api`](https://opentelemetry-python.readthedocs.io/) : l’API légère uniquement, de sorte que le middleware de traçage du SDK ne coûte rien tant que vous n’installez pas vous-même un SDK OpenTelemetry et un exporteur.
|
||||
* [`typing-extensions`](https://typing-extensions.readthedocs.io/) et [`typing-inspection`](https://pypi.org/project/typing-inspection/) : les fonctionnalités de typage modernes sous Python 3.10.
|
||||
* [`pywin32`](https://pypi.org/project/pywin32/) : Windows uniquement, utilisé pour la gestion des sous-processus `stdio`.
|
||||
|
||||
## Extras optionnels {#optional-extras}
|
||||
|
||||
* `mcp[cli]` ajoute [`typer`](https://typer.tiangolo.com/) et [`python-dotenv`](https://pypi.org/project/python-dotenv/) pour l’outil en ligne de commande `mcp` (`mcp dev`, `mcp run`, `mcp install`). Vous en aurez besoin pendant le développement ; vous pouvez vous en passer sur un serveur déployé.
|
||||
* `mcp[rich]` ajoute [`rich`](https://rich.readthedocs.io/) pour des journaux de serveur plus lisibles.
|
||||
186
i18n/fr/pages/get-started/real-host.md
Normal file
186
i18n/fr/pages/get-started/real-host.md
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
---
|
||||
translation:
|
||||
sections: [3c4f2f06b4e978b6, 22520eecae3d1961, f4e1709db18d635a, 2eb57992049671d9, 1ba83e9af37cc1b4, 4822586344b08d9e, 1c93afef72478992, b6b448f9eddd51dc, fe55370fd931815b]
|
||||
tool: 1
|
||||
---
|
||||
# Se connecter à un véritable hôte {#connect-to-a-real-host}
|
||||
|
||||
Un **hôte** est l’application dans laquelle votre serveur finit par vivre : Claude Desktop, Claude Code, un IDE. L’hôte est ce à quoi l’utilisateur parle. À l’intérieur, un **client** MCP lance votre serveur comme processus enfant et lui parle via le stdin et le stdout de ce processus.
|
||||
|
||||
Se connecter à un hôte se résume donc à un seul geste : vous lui indiquez **la commande qui démarre votre serveur**. Tout ce qui figure sur cette page (deux commandes CLI, trois fichiers JSON) n’est qu’un endroit différent où placer cette même commande.
|
||||
|
||||
## Un serveur, tous les hôtes {#one-server-every-host}
|
||||
|
||||
```python title="server.py" hl_lines="3 33-34"
|
||||
--8<-- "docs_src/real_host/tutorial001.py"
|
||||
```
|
||||
|
||||
Deux outils et une ressource, un seul fichier. Trois points concernant ce fichier comptent pour chaque hôte ci-dessous :
|
||||
|
||||
* `mcp.run()` sans argument démarre un serveur **stdio** : il bloque, lit les messages du protocole sur stdin et les écrit sur stdout. C’est le transport que parlent tous les hôtes de cette page. L’hôte démarre votre fichier comme processus enfant et possède ces deux tubes, c’est pourquoi se connecter ne revient jamais qu’à « voici la commande ». Vous ne choisissez jamais de port, et rien n’écoute sur un port.
|
||||
* `run()` est placé sous `if __name__ == "__main__":`. Tout ce qui suit **importe** ce fichier au lieu de l’exécuter ; un `run()` non protégé démarrerait donc un serveur dès que quoi que ce soit chargerait le module.
|
||||
* L’objet serveur est une variable globale de niveau module nommée `mcp`. C’est le nom que `mcp run` recherche (`server` et `app` fonctionnent aussi). Si vous l’appelez autrement, vous le nommez explicitement : `mcp run server.py:bookshop`.
|
||||
|
||||
C’est la dernière ligne de Python de cette page. À partir d’ici, tout n’est que configuration d’hôte.
|
||||
|
||||
## La commande de lancement {#the-launch-command}
|
||||
|
||||
Chaque hôte ci-dessous reçoit la même commande :
|
||||
|
||||
```bash
|
||||
uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
Une seule commande pour tous, parce que `uv run --with` résout le SDK dans un environnement neuf, à la volée : elle fonctionne depuis n’importe quel répertoire et n’a besoin d’aucun projet ni d’aucun environnement virtuel à activer. Cela compte ici plus qu’ailleurs, car un hôte lance votre serveur depuis *son* répertoire de travail avec un environnement presque vide, et non depuis votre shell.
|
||||
|
||||
C’est aussi la commande que `mcp install` écrit pour vous dans la configuration de Claude Desktop (ci-dessous) : ce que vous tapez à la main et ce que l’outil génère concordent, à l’exception de l’épinglage de version exact que l’outil ajoute.
|
||||
|
||||
!!! tip "Si un hôte ne trouve pas `uv`"
|
||||
Un hôte lance votre serveur avec un `PATH` minimal, et `uv` n’y figure peut-être pas.
|
||||
Remplacez le `uv` seul par le chemin absolu donné par `which uv` (macOS/Linux) ou `where uv`
|
||||
(Windows). C’est exactement ce qu’écrit `mcp install`.
|
||||
|
||||
!!! note "Cette page traite du cas local"
|
||||
Tout ici exécute votre serveur sur la machine où se trouve l’hôte : l’hôte lance votre
|
||||
fichier, via stdio. C’est exactement ce qu’il faut pour un outil personnel ou limité à une
|
||||
seule machine. Pour mettre un serveur à disposition de personnes qui n’ont *pas* votre
|
||||
fichier, vous distribuez une **URL**, pas une commande : le même objet `mcp`, servi via
|
||||
Streamable HTTP. **[Exécuter votre serveur](../run/index.md)** résume cette décision en un
|
||||
tableau, et **[Déployer et passer à l’échelle](../run/deploy.md)** est le chemin qui mène de
|
||||
là à un véritable nom d’hôte.
|
||||
|
||||
Et un hôte n’est rien de plus qu’une application contenant un client MCP ; votre propre
|
||||
code Python peut donc jouer le rôle de l’hôte : **[Transports du client](../client/transports.md)**
|
||||
lance ce même fichier comme sous-processus avec `stdio_client(...)`, et **[Tests](testing.md)**
|
||||
s’y connecte en mémoire, sans aucun processus.
|
||||
|
||||
## Claude Desktop {#claude-desktop}
|
||||
|
||||
Le seul hôte que le SDK peut configurer pour vous :
|
||||
|
||||
```bash
|
||||
uv run mcp install server.py
|
||||
```
|
||||
|
||||
C’est tout. `mcp install` importe le fichier pour lire le nom du serveur, trouve le fichier de configuration de Claude Desktop et y écrit la commande de lancement. Au passage, il convertit votre chemin en chemin absolu, pour que vous n’ayez pas à le faire.
|
||||
|
||||
Il n’y a là rien de mystérieux. Voici l’entrée qu’il écrit :
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"Bookshop": {
|
||||
"command": "/absolute/path/to/uv",
|
||||
"args": [
|
||||
"run",
|
||||
"--frozen",
|
||||
"--with",
|
||||
"mcp[cli]==2.0.0",
|
||||
"mcp",
|
||||
"run",
|
||||
"/absolute/path/to/server.py"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
C’est la commande de lancement de la section précédente avec trois ajouts : le chemin absolu vers `uv`, `--frozen` pour que `uv` ne réécrive jamais un fichier de verrouillage qui se trouverait à proximité, et un épinglage exact de la version de `mcp` que vous avez installée. Elle atterrit dans `claude_desktop_config.json`, qui se trouve ici :
|
||||
|
||||
* **macOS** : `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
* **Windows** : `%APPDATA%\Claude\claude_desktop_config.json`
|
||||
|
||||
Vous pouvez écrire ce fichier à la main. `mcp install` existe pour vous éviter l’erreur classique (un chemin relatif) en le faisant.
|
||||
|
||||
Quittez complètement Claude Desktop (pas seulement sa fenêtre), puis rouvrez-le.
|
||||
|
||||
!!! warning
|
||||
`mcp install` échoue avec `Claude app not found` si le *répertoire* de configuration de
|
||||
Claude Desktop n’existe pas encore. Installez Claude Desktop et lancez-le une fois : c’est
|
||||
ce qui crée le répertoire.
|
||||
|
||||
!!! tip
|
||||
Claude Desktop démarre votre serveur dans son propre processus ; les variables
|
||||
d’environnement de votre shell n’y sont donc pas. `uv run mcp install server.py -v API_KEY=abc123`
|
||||
(ou `-f .env`) les enregistre dans le champ `env` de l’entrée. `--name` remplace le nom de
|
||||
l’entrée ; par défaut, c’est le `name` du serveur.
|
||||
|
||||
## Claude Code {#claude-code}
|
||||
|
||||
Il n’y a aucun fichier à modifier. Enregistrez le serveur avec la CLI `claude` ; tout ce qui suit `--` est la commande de lancement.
|
||||
|
||||
```bash
|
||||
claude mcp add bookshop -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
Exécutez `/mcp` dans une session Claude Code pour confirmer que `bookshop` est connecté et que ses outils sont listés.
|
||||
|
||||
## Cursor {#cursor}
|
||||
|
||||
Créez `.cursor/mcp.json` à la racine de votre projet.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"bookshop": {
|
||||
"command": "uv",
|
||||
"args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Les mêmes `command` et `args`, sous la même clé `mcpServers` que celle qu’utilise Claude Desktop. Le serveur apparaît dans les paramètres MCP de Cursor avec les deux outils listés.
|
||||
|
||||
## VS Code {#vs-code}
|
||||
|
||||
Créez `.vscode/mcp.json` à la racine de votre projet.
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"bookshop": {
|
||||
"type": "stdio",
|
||||
"command": "uv",
|
||||
"args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Deux différences avec le fichier de Cursor, et ce sont les deux seules : la clé englobante est `servers`, et non `mcpServers`, et chaque entrée déclare son `type`. Acceptez la demande de confiance, puis **MCP: List Servers** dans la palette de commandes affiche `bookshop` en cours d’exécution.
|
||||
|
||||
!!! note
|
||||
Il vous faut VS Code 1.99 ou ultérieur avec l’extension **GitHub Copilot** connectée
|
||||
(Copilot Free suffit), et Copilot Chat doit être en mode **Agent**, car aucun autre mode
|
||||
n’appelle d’outils.
|
||||
|
||||
## Le serveur n’apparaît pas {#it-doesnt-show-up}
|
||||
|
||||
Avant de toucher à la moindre configuration d’hôte, exécutez vous-même la commande de lancement :
|
||||
|
||||
```bash
|
||||
uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
|
||||
```
|
||||
|
||||
Rien ne s’affiche, et la commande ne rend pas la main. Ce silence est normal : un serveur stdio attend qu’un hôte parle en premier sur stdin (`Ctrl-C` pour l’arrêter). Une trace d’erreur ou une sortie immédiate, voilà le vrai bogue, et vous pouvez désormais le lire au lieu de le deviner à travers un hôte.
|
||||
|
||||
Une fois que cette commande se contente d’attendre, ce qui reste est presque toujours l’une de ces trois causes :
|
||||
|
||||
* **Un chemin relatif.** L’hôte lance votre serveur depuis *son* répertoire de travail, pas depuis celui d’où vous l’avez enregistré. `server.py` là où il faut `/absolute/path/to/server.py` est, de loin, l’échec le plus fréquent. Si l’hôte ne trouve pas `uv` non plus, ce chemin doit lui aussi être absolu.
|
||||
* **L’hôte utilise encore son ancienne configuration.** Les hôtes lisent leur configuration au démarrage. Claude Desktop, en particulier, doit être *complètement quitté* (pas seulement sa fenêtre fermée) puis rouvert avant qu’une modification de `claude_desktop_config.json` prenne effet.
|
||||
* **Quelque chose a atteint stdout en dehors de la fenêtre de redirection.** En stdio, stdout *est* le protocole. Le SDK redirige vers stderr la sortie parasite vidée pendant qu’il sert, mais une sortie vidée sur stdout avant cela (un script d’enrobage qui fait un echo, un `print()` à l’import dans un processus sans tampon), ou un `print()` mis en tampon et vidé à la sortie de l’interpréteur, remet à l’hôte un message corrompu et celui-ci coupe la connexion. Journalisez avec la configuration `logging` par défaut, dont le gestionnaire stderr vide chaque enregistrement ; les gestionnaires personnalisés doivent eux aussi éviter stdout. Tous les détails sont dans **[Journalisation](../handlers/logging.md)**.
|
||||
|
||||
Claude Desktop tient un journal par serveur : `mcp-server-<NAME>.log` est le stderr de votre serveur, à côté de `mcp.log` pour les connexions, sous `~/Library/Logs/Claude` sur macOS et `%APPDATA%\Claude\logs` sur Windows.
|
||||
|
||||
Pour tout ce qui dépasse ces trois cas, la page à consulter est **[Dépannage](../troubleshooting.md)**.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* Un **hôte** (Claude Desktop, un IDE) exécute un client MCP qui lance votre serveur comme processus enfant via stdio. Se connecter, c’est lui donner une commande de lancement.
|
||||
* Cette commande est `uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py` : aucun venv à activer, elle fonctionne depuis n’importe quel répertoire.
|
||||
* **Claude Desktop** est le seul hôte que `mcp install` configure pour vous. Il écrit cette même commande (plus le chemin absolu vers `uv`, `--frozen` et un épinglage exact de la version que vous avez installée) dans `claude_desktop_config.json`, pour que vous n’ayez jamais à le faire.
|
||||
* **Claude Code**, c’est `claude mcp add bookshop -- <launch command>`. **Cursor**, c’est `.cursor/mcp.json` sous `mcpServers`. **VS Code**, c’est `.vscode/mcp.json` sous `servers`, chaque entrée avec un `type`.
|
||||
* Des chemins absolus partout, redémarrez l’hôte après avoir modifié sa configuration, et ne laissez jamais rien d’autre que le SDK écrire sur stdout.
|
||||
|
||||
Tous les hôtes de cette page se sont connectés au même fichier, avec la même commande. Ce que ce fichier peut *exposer*, c’est le reste de cette documentation : **[Outils](../servers/tools.md)**, **[Ressources](../servers/resources.md)**, et tous les transports autres que stdio dans **[Exécuter votre serveur](../run/index.md)**.
|
||||
117
i18n/fr/pages/get-started/testing.md
Normal file
117
i18n/fr/pages/get-started/testing.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
---
|
||||
translation:
|
||||
sections: ['4926721070127497', c52a1de2b6b32f40, 2e410b412c25f314, 627195f7159e24ef]
|
||||
tool: 1
|
||||
---
|
||||
# Tests {#testing}
|
||||
|
||||
Le SDK Python fournit une classe `Client` dotée d’un **transport en mémoire** : passez-lui votre objet serveur et il s’y connecte directement.
|
||||
|
||||
Pas de sous-processus. Pas de port. Pas de transport du tout. C’est la même idée que le `TestClient` de FastAPI.
|
||||
|
||||
## Utilisation de base {#basic-usage}
|
||||
|
||||
Supposons que vous ayez un serveur simple avec un seul outil (tool) :
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/testing/tutorial001.py"
|
||||
```
|
||||
|
||||
Pour exécuter le test ci-dessous, vous aurez besoin de deux dépendances (de développement) supplémentaires :
|
||||
|
||||
=== "uv"
|
||||
|
||||
```bash
|
||||
uv add --dev pytest inline-snapshot
|
||||
```
|
||||
|
||||
=== "pip"
|
||||
|
||||
```bash
|
||||
pip install pytest inline-snapshot
|
||||
```
|
||||
|
||||
!!! info
|
||||
Cette documentation suppose que vous connaissez déjà [`pytest`](https://docs.pytest.org/en/stable/).
|
||||
|
||||
[`inline-snapshot`](https://15r10nk.github.io/inline-snapshot/latest/) est ce que le test
|
||||
ci-dessous utilise pour vérifier l’objet résultat entier en une seule ligne. Il enregistre la
|
||||
sortie d’un test sous la forme du littéral `snapshot(...)` que vous voyez. Si vous préférez vous
|
||||
en passer, supprimez l’import et vérifiez les champs qui vous intéressent
|
||||
(`result.content[0].text == "3"`) comme dans n’importe quel autre test.
|
||||
|
||||
Voici maintenant le test :
|
||||
|
||||
```python title="test_server.py"
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
from mcp import Client
|
||||
from mcp.types import CallToolResult, TextContent
|
||||
|
||||
from server import mcp
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend(): # (1)!
|
||||
return "asyncio"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client(): # (2)!
|
||||
async with Client(mcp, raise_exceptions=True) as c:
|
||||
yield c
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_call_add_tool(client: Client):
|
||||
result = await client.call_tool("add", {"a": 1, "b": 2})
|
||||
# Drop the server identity stamp in `_meta`; it is not what this test is about.
|
||||
result.meta = None
|
||||
assert result == snapshot(
|
||||
CallToolResult(
|
||||
content=[TextContent(type="text", text="3")],
|
||||
structured_content={"result": 3},
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
1. Si vous utilisez `trio`, renvoyez `"trio"` à la place. Consultez la [documentation d’anyio](https://anyio.readthedocs.io/en/stable/testing.html#specifying-the-backends-to-run-on) pour les détails.
|
||||
2. La fixture produit un client connecté. Chaque test qui prend `client` en paramètre obtient une nouvelle connexion en mémoire vers le même serveur.
|
||||
|
||||
Et voilà. Vous pouvez maintenant étendre vos tests pour couvrir davantage de scénarios.
|
||||
|
||||
## Pourquoi `raise_exceptions=True` ? {#why-raise_exceptionstrue}
|
||||
|
||||
Deux choses différentes peuvent mal tourner, et cet indicateur n’en concerne qu’une seule.
|
||||
|
||||
Une exception dans l’un de **vos outils** n’est pas un échec du protocole. Elle devient un résultat
|
||||
normal avec `is_error=True`, et le modèle lit le message. `raise_exceptions` n’y change rien : avec
|
||||
ou sans lui, `call_tool` renvoie le même résultat `is_error=True`. Une page entière y est
|
||||
consacrée : **[Gérer les erreurs](../servers/handling-errors.md)**.
|
||||
|
||||
Un échec **en dehors** du corps d’un outil est différent. Sur la connexion que vous donne
|
||||
`Client(mcp)`, le serveur le neutralise en un `"Internal server error"` générique avant que le
|
||||
client ne le voie. Vous ne devriez jamais divulguer les détails d’un plantage inattendu à un
|
||||
appelant distant. Dans un test, c’est exactement ce que vous ne voulez *pas*, et c’est ce que
|
||||
change `raise_exceptions=True` : votre test voit le vrai message au lieu de la version neutralisée.
|
||||
|
||||
Laissez-le activé dans les tests. Il n’a aucun sens dans du code de production.
|
||||
|
||||
## Dans le processus par défaut {#in-process-by-default}
|
||||
|
||||
!!! note
|
||||
`Client(mcp)` se connecte dans le processus et est **neutre vis-à-vis de la génération du
|
||||
protocole** par défaut : il sonde le serveur et choisit le chemin de protocole approprié. Fixez
|
||||
`mode="legacy"` si votre test exerce une sémantique propre aux connexions historiques (push
|
||||
d’échantillonnage (sampling) ou d’élicitation (elicitation), `message_handler`), et retirez alors
|
||||
`raise_exceptions=True` : une connexion historique ne neutralise jamais rien, et l’indicateur
|
||||
relève l’échec dans la tâche du serveur plutôt que dans votre test.
|
||||
|
||||
Cette unique ligne est aussi la raison pour laquelle cette documentation peut vous promettre que
|
||||
ses exemples fonctionnent : chaque fichier d’exemple est exercé par la propre suite de tests du
|
||||
SDK, presque tous via ce client précisément. Vous utilisez le même outil que le SDK utilise sur
|
||||
lui-même.
|
||||
|
||||
Vous avez un serveur qui fonctionne et qui est testé. L’intégrer dans une véritable application
|
||||
(Claude Desktop, un IDE), c’est **[Se connecter à un hôte réel](real-host.md)** ; toutes les autres
|
||||
manières de le servir sont dans **[Exécuter votre serveur](../run/index.md)**.
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue