docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
6 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)**.
|
||||
Loading…
Reference in a new issue