docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
9 changed files with 89737 additions and 54 deletions
130
i18n/fr/pages/servers/completions.md
Normal file
130
i18n/fr/pages/servers/completions.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
---
|
||||
translation:
|
||||
sections: [72f9c964769076dd, 9a2c14e10935b515, 235299eb78ab12d7, 8aee1e78c8237fb8, 9bd86acd4112138f, 55343cb7f250dc7b]
|
||||
tool: 1
|
||||
---
|
||||
# Complétions {#completions}
|
||||
|
||||
Un client qui construit une interface utilisateur au-dessus de votre serveur veut autocompléter les valeurs des arguments au fil de la saisie de l’utilisateur : noms de langages, noms de dépôts, chemins de fichiers.
|
||||
|
||||
Les **complétions** sont le moyen par lequel votre serveur fournit ces suggestions.
|
||||
|
||||
## Quelque chose à compléter {#something-worth-completing}
|
||||
|
||||
Les complétions s’appliquent à exactement deux choses : les arguments d’un **prompt** et les paramètres d’un **modèle de ressource**. Commencez donc par un serveur qui en possède un de chaque :
|
||||
|
||||
```python title="server.py" hl_lines="6 12"
|
||||
--8<-- "docs_src/completions/tutorial001.py"
|
||||
```
|
||||
|
||||
Rien ici ne concerne encore les complétions.
|
||||
|
||||
* `review_code` prend un `language`. Un utilisateur ne devrait pas avoir à deviner quelles orthographes vous acceptez.
|
||||
* `github_repo` prend un `owner` et un `repo`. Des champs de texte libre pour les deux font un mauvais formulaire.
|
||||
|
||||
## Le gestionnaire de complétion {#the-completion-handler}
|
||||
|
||||
Ajoutez **une** seule fonction décorée avec `@mcp.completion()` :
|
||||
|
||||
```python title="server.py" hl_lines="21-29"
|
||||
--8<-- "docs_src/completions/tutorial002.py"
|
||||
```
|
||||
|
||||
* Il y a un seul gestionnaire (handler) par serveur. Chaque requête de complétion arrive ici, et vous aiguillez selon ce qui est en cours de complétion.
|
||||
* Il doit être `async def` : le SDK l’attend avec await.
|
||||
* Il reçoit trois arguments :
|
||||
* `ref` : *quel* prompt ou modèle de ressource, sous la forme d’une `PromptReference` ou d’une `ResourceTemplateReference`. C’est `isinstance` qui vous permet de les distinguer.
|
||||
* `argument` : `argument.name` est l’argument en cours de complétion, `argument.value` est ce que l’utilisateur a saisi jusqu’ici.
|
||||
* `context` : les arguments déjà résolus. Ignorez-le pour l’instant.
|
||||
* Vous renvoyez une `Completion(values=[...])`, ou `None` quand vous n’avez rien à proposer.
|
||||
|
||||
!!! tip
|
||||
`argument.value` est le préfixe que l’utilisateur a saisi. Le SDK ne filtre **pas** pour vous : ce que
|
||||
vous mettez dans `values` est ce que l’interface affiche. Le `startswith`, c’est à vous de l’écrire.
|
||||
|
||||
### Essayer {#try-it}
|
||||
|
||||
Pilotez-le avec le `Client` en mémoire de **[Tests](../get-started/testing.md)**. Appelez
|
||||
`client.complete()` avec `ref=PromptReference(name="review_code")` et
|
||||
`argument={"name": "language", "value": "py"}` :
|
||||
|
||||
```python
|
||||
result.completion.values # ['python']
|
||||
```
|
||||
|
||||
* `ref` est le même type de référence que celui que reçoit votre gestionnaire.
|
||||
* `argument` est un simple dict avec exactement deux clés, `name` et `value`.
|
||||
|
||||
Envoyez une `value` vide et vous obtenez toute la liste en retour. `lang.startswith("")` est vrai pour chaque langage :
|
||||
|
||||
```python
|
||||
result.completion.values # ['go', 'javascript', 'python', 'rust', 'typescript']
|
||||
```
|
||||
|
||||
Interrogez-le sur `code` (un argument que votre gestionnaire ne reconnaît pas) et il renvoie `None`, que le SDK transforme en liste vide :
|
||||
|
||||
```python
|
||||
result.completion.values # []
|
||||
```
|
||||
|
||||
`None` signifie *« aucune suggestion »*, jamais une erreur. Une interface se rabat sur un simple champ de texte.
|
||||
|
||||
## Une capacité que vous n’avez jamais déclarée {#a-capability-you-never-declared}
|
||||
|
||||
Enregistrer le gestionnaire, c’est la déclarer. Connectez un client et regardez :
|
||||
|
||||
```python
|
||||
client.server_capabilities.completions # CompletionsCapability()
|
||||
```
|
||||
|
||||
Vous n’avez listé `completions` nulle part. Le SDK a vu le gestionnaire et a déclaré la capacité pour vous. Toutes les capacités *optionnelles* fonctionnent ainsi : le gestionnaire est la déclaration. (Les trois primitives ne sont pas optionnelles : `MCPServer` les déclare toujours, gestionnaires ou non.)
|
||||
|
||||
!!! check
|
||||
Revenez au premier `server.py` (celui sans gestionnaire) et interrogez-le quand même. L’appel échoue
|
||||
avec une erreur JSON-RPC :
|
||||
|
||||
```text
|
||||
Method not found
|
||||
```
|
||||
|
||||
Et `client.server_capabilities.completions` vaut `None`. C’est tout l’intérêt de la capacité : un
|
||||
client bien conçu la vérifie et n’envoie jamais la requête à laquelle vous ne pouvez pas répondre.
|
||||
|
||||
## Arguments dépendants {#dependent-arguments}
|
||||
|
||||
`github://repos/{owner}/{repo}` a deux paramètres, et les valeurs utiles pour `repo` dépendent du `owner` choisi en premier.
|
||||
|
||||
C’est à cela que sert `context`. Il transporte les arguments que l’utilisateur a **déjà résolus** :
|
||||
|
||||
```python title="server.py" hl_lines="8-11 34-38"
|
||||
--8<-- "docs_src/completions/tutorial003.py"
|
||||
```
|
||||
|
||||
* La nouvelle branche se déclenche pour le paramètre `repo` du modèle.
|
||||
* `context.arguments` est un `dict[str, str] | None` des valeurs choisies jusqu’ici (ici, `owner`).
|
||||
* Pas encore de `owner` signifie pas de suggestion pertinente, donc le gestionnaire renvoie `None`.
|
||||
|
||||
Le client envoie ces valeurs résolues avec `context_arguments=`. Cette fois, `ref` est une
|
||||
`ResourceTemplateReference(uri="github://repos/{owner}/{repo}")`. Demandez `repo` avec une
|
||||
`value` vide et passez `context_arguments={"owner": "modelcontextprotocol"}` :
|
||||
|
||||
```python
|
||||
result.completion.values # ['python-sdk', 'typescript-sdk', 'inspector']
|
||||
```
|
||||
|
||||
Retirez `context_arguments=` et le même appel renvoie `[]`. Le gestionnaire ne peut pas savoir quels dépôts proposer tant qu’il ne connaît pas le propriétaire.
|
||||
|
||||
!!! info
|
||||
`Completion` accepte aussi `total=` et `has_more=`. Renseignez-les quand `values` est une tranche d’une liste
|
||||
plus longue, pour qu’une interface puisse afficher *« et 200 de plus »*. La plupart des gestionnaires n’en ont jamais besoin.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* Les complétions sont des suggestions pour les **arguments de prompt** et les **paramètres de modèle de ressource**. Rien d’autre.
|
||||
* `@mcp.completion()` enregistre l’unique gestionnaire. Sa signature est `async def (ref, argument, context) -> Completion | None`.
|
||||
* Aiguillez sur `isinstance(ref, ...)` et sur `argument.name`. Filtrez vous-même selon `argument.value`.
|
||||
* `None` devient une liste vide. Ce n’est jamais une erreur.
|
||||
* `context.arguments` contient les valeurs déjà résolues ; le client les fournit via `context_arguments=`.
|
||||
* La capacité `completions` apparaît dès que vous enregistrez le gestionnaire. Sans lui, la requête reçoit `Method not found`.
|
||||
|
||||
Les suggestions aident pendant que l’utilisateur *remplit* encore un prompt ou un modèle ; pour lui poser une question au *milieu* d’un appel d’outil, c’est l’**[élicitation (elicitation)](../handlers/elicitation.md)** qu’il vous faut. Tout ce qu’un outil peut renvoyer en plus du texte se trouve dans **[Images, audio et icônes](media.md)**.
|
||||
139
i18n/fr/pages/servers/handling-errors.md
Normal file
139
i18n/fr/pages/servers/handling-errors.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
---
|
||||
translation:
|
||||
sections: [e33d441f12d50535, 7099694c603e0f5f, c1df4cf9673433e6, c9cd294541422e6e, 6cec073617bfd037, efa92b8f99e908c8, 6a22a29e27fb4601]
|
||||
tool: 1
|
||||
---
|
||||
# Gérer les erreurs {#handling-errors}
|
||||
|
||||
Un outil (tool) peut échouer de deux manières, et le SDK les traite très différemment.
|
||||
|
||||
Levez une exception ordinaire et c’est le **modèle** qui la voit. Levez `MCPError` et c’est le **protocole** qui la voit.
|
||||
|
||||
Cette page vous aide à choisir.
|
||||
|
||||
## Une erreur que le modèle peut corriger {#an-error-the-model-can-fix}
|
||||
|
||||
Prenez un outil qui effectue une recherche, et laissez cette recherche échouer :
|
||||
|
||||
```python title="server.py" hl_lines="11-12"
|
||||
--8<-- "docs_src/handling_errors/tutorial001.py"
|
||||
```
|
||||
|
||||
Ces deux lignes n’ont rien de spécifique à MCP. `get_author` lève une simple `ValueError`, comme le ferait n’importe quelle fonction Python.
|
||||
|
||||
Appelez-le avec un titre absent du catalogue et regardez le résultat :
|
||||
|
||||
```python
|
||||
result.is_error # True
|
||||
result.content # [TextContent(text="Error executing tool get_author: No book titled 'Nothing' in the catalog.")]
|
||||
result.structured_content # None
|
||||
```
|
||||
|
||||
* La requête a **réussi**. Il y a un résultat ; rien n’a été levé côté appelant.
|
||||
* `is_error` vaut `True`, et le message de votre exception (préfixé du nom de l’outil) se trouve dans `content`, exactement là où le modèle lit.
|
||||
* `structured_content` vaut `None`. Un appel en échec n’a aucune valeur de retour à structurer.
|
||||
|
||||
C’est une **erreur d’outil** (tool error), et c’est le comportement par défaut pour *toute* exception que lève votre outil. C’est aussi presque toujours ce que vous voulez.
|
||||
|
||||
C’est le modèle qui appelle votre outil. C’est lui qui a choisi les arguments. Une erreur d’outil est donc un tour de conversation : le modèle lit *« No book titled 'Nothing' in the catalog. »*, comprend qu’il s’est trompé de titre et rappelle l’outil avec un meilleur. Vous avez écrit un seul `raise` et obtenu un agent qui se corrige tout seul.
|
||||
|
||||
!!! tip
|
||||
N’utilisez jamais `return` pour renvoyer un message d’erreur depuis un outil. Une chaîne renvoyée a
|
||||
`is_error=False` : pour le modèle (et pour toute interface cliente), l’outil semble avoir
|
||||
fonctionné et cette chaîne semble être la réponse. Utilisez `raise`. C’est le drapeau qui fait signal.
|
||||
|
||||
## Une erreur que le modèle ne peut pas corriger {#an-error-the-model-cannot-fix}
|
||||
|
||||
Remplacez maintenant `ValueError` par `MCPError`.
|
||||
|
||||
```python title="server.py" hl_lines="1 3 14"
|
||||
--8<-- "docs_src/handling_errors/tutorial002.py"
|
||||
```
|
||||
|
||||
`MCPError` est l’**erreur de protocole** du SDK. C’est la seule exception que l’enveloppe de l’outil n’intercepte *pas* : elle se propage, et toute la requête `tools/call` échoue avec une erreur JSON-RPC au lieu d’un résultat.
|
||||
|
||||
```json
|
||||
{
|
||||
"code": -32602,
|
||||
"message": "No book titled 'Nothing' in the catalog."
|
||||
}
|
||||
```
|
||||
|
||||
* Il n’y a **aucun résultat**. Pas de `content`, pas de `is_error` : rien à lire pour le modèle.
|
||||
* C’est l’application **hôte** qui reçoit l’erreur, exactement comme si l’outil n’existait pas du tout.
|
||||
* `code`, `message` et `data` arrivent intacts. `INVALID_PARAMS` vaut `-32602` ; `mcp.types` l’exporte, avec les autres codes d’erreur JSON-RPC (`INVALID_REQUEST`, `INTERNAL_ERROR`, …), sous forme de constantes pour que vous n’ayez jamais à saisir de nombre magique.
|
||||
|
||||
!!! check
|
||||
Même recherche, même échec, mais cette fois l’appel *lève une exception* côté client au lieu de renvoyer un résultat :
|
||||
|
||||
```text
|
||||
mcp.shared.exceptions.MCPError: No book titled 'Nothing' in the catalog.
|
||||
```
|
||||
|
||||
La première version donnait au modèle une phrase à laquelle réagir. Celle-ci ne lui donne rien.
|
||||
Pour `get_author`, c’est strictement pire, et c’est tout l’objet de la section suivante.
|
||||
|
||||
## Laquelle lever {#which-one-to-raise}
|
||||
|
||||
Les deux voies répondent à deux questions différentes.
|
||||
|
||||
* **Levez n’importe quelle exception** pour un échec d’*exécution* : ce que votre outil a tenté de faire n’a pas fonctionné. Le modèle a choisi l’appel, il devrait donc en voir la conséquence et avoir une chance de se rattraper. Un titre mal orthographié, une API amont qui a expiré, une ligne qui n’existe pas : autant d’erreurs d’outil.
|
||||
* **Levez `MCPError`** quand c’est la *requête elle-même* qui doit être rejetée : il manque au client une capacité dont dépend votre outil, le serveur n’est pas en état de servir qui que ce soit, l’appelant a sauté une étape obligatoire. Aucune nouvelle tentative du modèle ne corrige cela, il n’y a donc rien à gagner à lui transmettre le message.
|
||||
|
||||
Une seule question tranche : **un modèle plus malin aurait-il pu éviter cela ?** Oui -> exception ordinaire. Non -> `MCPError`.
|
||||
|
||||
Selon ce critère, la seconde version de `get_author` a fait le mauvais choix : un meilleur titre règle le problème, le modèle méritait donc de voir le message. Elle est là pour vous montrer le mécanisme, pas pour le recommander.
|
||||
|
||||
!!! info
|
||||
`MCPError` s’importe avec `from mcp import MCPError` et prend `code`, `message` et une charge
|
||||
utile `data` facultative. Ce que vous y mettez est ce que le client reçoit : le SDK transmet telle
|
||||
quelle une `MCPError` levée au lieu de l’assainir.
|
||||
|
||||
## Une ressource qui n’existe pas {#a-resource-that-doesnt-exist}
|
||||
|
||||
Les ressources tracent la même frontière, et fournissent une exception dédiée pour le cas courant.
|
||||
|
||||
```python title="server.py" hl_lines="2 13"
|
||||
--8<-- "docs_src/handling_errors/tutorial003.py"
|
||||
```
|
||||
|
||||
`books://{title}` est un **modèle** (template). Il correspond à *n’importe quel* titre, donc « l’URI est bien formé » et « le livre existe » sont deux questions différentes, et seule votre fonction peut répondre à la seconde.
|
||||
|
||||
Quand elle ne le peut pas, levez `ResourceNotFoundError`. Le SDK la transforme en l’erreur de protocole que la spécification attribue à une ressource manquante : `-32602` avec l’URI demandé dans `data`, pour que le client sache *quelle* lecture a échoué.
|
||||
|
||||
```json
|
||||
{
|
||||
"code": -32602,
|
||||
"message": "No book titled 'Nothing' in the catalog.",
|
||||
"data": {"uri": "books://Nothing"}
|
||||
}
|
||||
```
|
||||
|
||||
Remarquez qu’il n’y a pas ici de demi-résultat `is_error=True`. La lecture d’une ressource renvoie un contenu ou échoue : les ressources n’ont que la voie du protocole. Les modèles et tout ce qui concerne les ressources se trouvent dans **[Ressources](resources.md)**.
|
||||
|
||||
## Les erreurs que vous ne levez jamais {#errors-you-never-raise}
|
||||
|
||||
Un mauvais argument n’atteint jamais votre fonction.
|
||||
|
||||
Envoyez à `get_author` un `title` qui n’est pas une chaîne et le SDK le rejette d’après le schéma d’entrée **avant** de vous appeler, sous la forme du même genre d’erreur d’outil `is_error=True` que le modèle peut lire et corriger. **[Outils](tools.md)** montre le même rejet avec une contrainte `Field(le=50)`.
|
||||
|
||||
Cela représente toute une catégorie d’instructions `raise` que vous n’écrivez pas : ne revalidez pas vos propres annotations de type.
|
||||
|
||||
!!! info
|
||||
Tout ce que décrit cette page est ce qu’un **client** voit, et le `Client` en mémoire avec lequel
|
||||
vous écrirez vos tests voit exactement la même chose. Même `raise_exceptions=True` ne retransforme
|
||||
pas une erreur d’outil en traceback : au moment où ce drapeau pourrait agir, votre exception est déjà
|
||||
devenue le résultat `is_error=True`. Faites vos assertions sur le résultat. **[Tests](../get-started/testing.md)** présente ce schéma.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* Levez **n’importe quelle exception** dans un outil -> l’appel renvoie `is_error=True` avec votre message dans `content`. Le modèle le lit et peut réessayer. C’est le comportement par défaut.
|
||||
* Levez **`MCPError`** -> l’appel lui-même échoue avec une erreur JSON-RPC. Le modèle ne voit rien ; c’est l’hôte qui s’en occupe. `code`, `message` et `data` arrivent intacts.
|
||||
* La question qui tranche : *un modèle plus malin aurait-il pu éviter cela ?* Oui -> exception. Non -> `MCPError`.
|
||||
* `ResourceNotFoundError` depuis un gestionnaire (handler) de ressource -> le `-32602` du protocole, avec l’URI dans `data`.
|
||||
* Les mauvais arguments sont rejetés d’après le schéma avant que votre fonction ne s’exécute ; vous n’avez pas de `raise` à écrire pour eux.
|
||||
* `from mcp import MCPError` ; les constantes de codes d’erreur viennent de `mcp.types`.
|
||||
|
||||
Les erreurs sont gérées. C’est tout ce qu’un serveur *expose*. Ce que chaque gestionnaire peut lire, et faire en retour auprès du client pendant qu’il s’exécute, fait l’objet de la section suivante : **[Dans votre gestionnaire](../handlers/index.md)**.
|
||||
|
||||
Le texte exact des erreurs du SDK que vous avez le plus de chances de rencontrer, ce que chacune signifie et le correctif en un geste pour chacune se trouvent dans **[Dépannage](../troubleshooting.md)**.
|
||||
22
i18n/fr/pages/servers/index.md
Normal file
22
i18n/fr/pages/servers/index.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
translation:
|
||||
sections: [09defc170a0da89d]
|
||||
tool: 1
|
||||
---
|
||||
# Serveurs {#servers}
|
||||
|
||||
Un `MCPServer` expose trois primitives à un client connecté. Ce qui les distingue, c’est qui décide de les utiliser :
|
||||
|
||||
* Un **[outil (tool)](tools.md)** est une action que le *modèle* choisit et appelle. C’est la page que la plupart des lecteurs veulent en premier, et **[Sortie structurée](structured-output.md)** en est le complément de référence : tout sur la forme de ce qu’un outil renvoie.
|
||||
* Une **[ressource](resources.md)** est une donnée en lecture seule que l’*application* choisit de lire. **[Modèles d’URI](uri-templates.md)** en est le complément de référence : la syntaxe d’adressage complète et les règles de sécurité des chemins.
|
||||
* Un **[prompt](prompts.md)** est un modèle de message qu’une *personne* invoque par son nom, depuis un menu ou une commande slash.
|
||||
|
||||
Autour de ces trois primitives, voici le reste de ce qu’un serveur déclare :
|
||||
|
||||
* **[Complétions](completions.md)** décrit l’autocomplétion côté serveur des arguments de prompts et de modèles de ressources.
|
||||
* **[Images, audio et icônes](media.md)** couvre tout ce qu’un outil peut renvoyer en dehors du texte, ainsi que les icônes qu’un client affiche à côté de votre serveur.
|
||||
* **[Gérer les erreurs](handling-errors.md)** explique la différence entre une erreur dont le modèle peut se remettre et une erreur qu’il ne doit jamais voir.
|
||||
|
||||
Chaque page ici se suffit à elle-même ; allez directement à celle dont vous avez besoin. Si vous n’avez pas encore construit de serveur, commencez plutôt par **[Premiers pas](../get-started/first-steps.md)**.
|
||||
|
||||
Ce qui se passe *à l’intérieur* des fonctions que vous enregistrez (l’objet `Context`, l’injection de dépendances, demander à l’utilisateur des informations supplémentaires en cours d’appel) fait l’objet de la section suivante, **[Dans votre gestionnaire](../handlers/index.md)**.
|
||||
122
i18n/fr/pages/servers/media.md
Normal file
122
i18n/fr/pages/servers/media.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
translation:
|
||||
sections: [496394d24d221bf1, 4ceb4591180dc6c3, 0fd63e4682d02e0c, 969ede0bd3686a16, 043f526230dd243d, 6ee3e9bcfd24047a]
|
||||
tool: 1
|
||||
---
|
||||
# Médias {#media}
|
||||
|
||||
Le texte n’est pas la seule chose qu’un outil (tool) peut renvoyer.
|
||||
|
||||
Le SDK fournit deux utilitaires pour les résultats binaires (**`Image`** et **`Audio`**) et un type **`Icon`** pour donner un visage à votre serveur, à vos outils, à vos ressources et à vos prompts dans l’interface du client.
|
||||
|
||||
## Renvoyer une image {#returning-an-image}
|
||||
|
||||
Annotez le type de retour avec `Image`, pointez-le vers un fichier, et renvoyez-le :
|
||||
|
||||
```python title="server.py" hl_lines="8 12 14"
|
||||
--8<-- "docs_src/media/tutorial001.py"
|
||||
```
|
||||
|
||||
* `Image` prend exactement l’un des deux : `path` (un fichier à lire) ou `data` (des octets bruts).
|
||||
* Le type MIME que voit le client est deviné à partir de l’extension : `logo.png` est annoncé comme `image/png`.
|
||||
* Les logos n’ont rien de particulier ici. N’importe quel PNG placé à côté de `server.py` convient : un graphique que votre code a généré, un schéma, une photo.
|
||||
|
||||
`Image` est une commodité du SDK, pas un type du protocole. Sur la liaison, votre valeur de retour devient un bloc **`ImageContent`** (les octets du fichier encodés en base64, plus le type MIME) :
|
||||
|
||||
```python
|
||||
result.content # [ImageContent(type="image", data="iVBORw0KGgoAAAANSUhEUg...", mime_type="image/png")]
|
||||
result.structured_content # None
|
||||
```
|
||||
|
||||
Deux choses à remarquer :
|
||||
|
||||
* `data` est en base64. Vous n’avez jamais touché aux octets ; le SDK a lu le fichier et s’est chargé de l’encodage.
|
||||
* `structured_content` vaut `None`. Une `Image` est du contenu que le modèle regarde, pas des données que l’application analyse : il n’y a pas de schéma de sortie. (À comparer avec la **[Sortie structurée](structured-output.md)**, où l’annotation de retour *est* le schéma.)
|
||||
|
||||
!!! info
|
||||
`ImageContent` et `AudioContent` se trouvent dans `mcp.types`, juste à côté du `TextContent`
|
||||
que devient un simple résultat `str` (**[Outils](tools.md)**). Un résultat d’outil est une liste de blocs de contenu ; `Image` et `Audio` sont
|
||||
le moyen le plus court de produire les deux variantes binaires.
|
||||
|
||||
### Essayer {#try-it}
|
||||
|
||||
Déposez n’importe quel PNG à côté de `server.py`, nommez-le `logo.png`, et lancez :
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Ouvrez l’onglet **Tools** et appelez `logo`. Le résultat n’est pas une chaîne : c’est un bloc de contenu `image`, et l’Inspector affiche votre image. Tout ce qui s’est passé entre le fichier sur le disque et les pixels à l’écran, c’est le SDK.
|
||||
|
||||
## Renvoyer de l’audio {#returning-audio}
|
||||
|
||||
`Audio` a la même forme. Laissez `logo.png` là où il était, et placez n’importe quel WAV à côté, sous le nom `chime.wav` :
|
||||
|
||||
```python title="server.py" hl_lines="18-21"
|
||||
--8<-- "docs_src/media/tutorial002.py"
|
||||
```
|
||||
|
||||
Le résultat est un bloc **`AudioContent`** :
|
||||
|
||||
```python
|
||||
result.content # [AudioContent(type="audio", data="UklGR...", mime_type="audio/wav")]
|
||||
result.structured_content # None
|
||||
```
|
||||
|
||||
Même principe : un fichier sur le disque en entrée, du base64 et un type MIME en sortie, pas de schéma de sortie.
|
||||
|
||||
## Des octets ou un fichier {#bytes-or-a-file}
|
||||
|
||||
Les deux utilitaires acceptent aussi `data=` (des octets bruts) à la place de `path=`. C’est le mode prévu pour des octets qui n’ont jamais eu de fichier à eux — une colonne de base de données, une réponse HTTP, quelque chose que Pillow vient de dessiner :
|
||||
|
||||
```python title="server.py" hl_lines="14 15"
|
||||
--8<-- "docs_src/media/tutorial003.py"
|
||||
```
|
||||
|
||||
Avec `path=`, il n’y a rien à déclarer : le fichier est lu au moment où le résultat est construit, et le type MIME est deviné à partir de l’extension :
|
||||
|
||||
* `Image` : `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`.
|
||||
* `Audio` : `.wav`, `.mp3`, `.ogg`, `.flac`, `.aac`, `.m4a`.
|
||||
|
||||
Une extension qu’il ne reconnaît pas se rabat sur `application/octet-stream`.
|
||||
|
||||
!!! check
|
||||
Avec `data=`, il n’y a pas de nom de fichier, donc rien à partir de quoi deviner. Oubliez `format=` et
|
||||
le SDK se rabat sur une valeur par défaut : `image/png` pour les images, `audio/wav` pour l’audio. Construisez un
|
||||
`Audio` à partir d’octets MP3 de cette façon et le client reçoit `mime_type="audio/wav"`, puis
|
||||
échoue consciencieusement à le décoder. Quand vous passez `data=`, passez `format=`.
|
||||
|
||||
## Icônes {#icons}
|
||||
|
||||
Une `Icon` est une métadonnée, pas du contenu. Elle ne transporte pas l’image ; elle en désigne une par un URI, et un client peut la récupérer et l’afficher à côté du nom de votre serveur, d’un outil, d’une ressource ou d’un prompt.
|
||||
|
||||
```python title="server.py" hl_lines="4-5 7 10 16"
|
||||
--8<-- "docs_src/media/tutorial004.py"
|
||||
```
|
||||
|
||||
* `src` est un URI que le client peut résoudre : `https:`, ou un URI `data:` si vous voulez l’icône embarquée sans récupération supplémentaire.
|
||||
* `mime_type` et `sizes` (`"48x48"`, ou `"any"` pour un format vectoriel) permettent au client de choisir la bonne lorsque vous en proposez plusieurs.
|
||||
* `theme="light"` ou `theme="dark"` réserve une icône à un jeu de couleurs.
|
||||
|
||||
Le même mot-clé `icons=[...]` est accepté par `MCPServer(...)`, `@mcp.tool()`, `@mcp.resource()` et `@mcp.prompt()`.
|
||||
|
||||
### Où un client les voit {#where-a-client-sees-them}
|
||||
|
||||
Les icônes voyagent avec ce qu’elles décorent. Celles du serveur arrivent quand le client se connecte, sur `client.server_info` (facultatif sur les connexions de génération 2026, donc restreignez d’abord le type) :
|
||||
|
||||
```python
|
||||
assert client.server_info is not None # python-sdk servers identify themselves by default
|
||||
client.server_info.icons # [Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"])]
|
||||
```
|
||||
|
||||
Les icônes d’un outil sont sur l’objet `Tool` issu de `tools/list`, celles d’une ressource sur le `Resource` issu de `resources/list`, celles d’un prompt sur le `Prompt` issu de `prompts/list`. Le champ s’appelle toujours `icons`.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* Renvoyez une `Image` ou un `Audio` depuis un outil et le client reçoit un bloc `ImageContent` / `AudioContent` : vos octets encodés en base64, avec un type MIME.
|
||||
* Construisez-en un à partir d’un `path=` et laissez l’extension décider du type MIME, ou à partir de `data=` en mémoire plus un `format=` explicite.
|
||||
* Les résultats média ne portent ni `structured_content` ni schéma de sortie.
|
||||
* Une `Icon` est un pointeur : un URI `src` plus, en option, `mime_type`, `sizes` et `theme`.
|
||||
* `icons=[...]` fonctionne sur le serveur, sur les outils, sur les ressources et sur les prompts, et les clients les retrouvent sur les objets correspondants.
|
||||
|
||||
C’est tout ce qu’un outil peut mettre *dans* un résultat. Ce qui se passe quand un outil *échoue* (et qui doit l’apprendre), c’est **[Gérer les erreurs](handling-errors.md)**.
|
||||
155
i18n/fr/pages/servers/prompts.md
Normal file
155
i18n/fr/pages/servers/prompts.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
---
|
||||
translation:
|
||||
sections: [d65c098f37f5b6c3, dd0c2724d6f2877e, 6835bb3570c6714c, ffe823cb0fedd488, f33651add1b59094]
|
||||
tool: 1
|
||||
---
|
||||
# Prompts {#prompts}
|
||||
|
||||
Un **prompt** est un modèle de message que l’utilisateur choisit.
|
||||
|
||||
Les outils sont destinés au modèle. Un prompt, c’est l’inverse : l’utilisateur en choisit un dans un menu de son client (une commande slash, un bouton), renseigne ses arguments, et les messages rendus entrent dans la conversation comme s’il les avait saisis lui-même.
|
||||
|
||||
Vous en déclarez un en plaçant `@mcp.prompt()` sur une fonction qui renvoie le texte.
|
||||
|
||||
## Votre premier prompt {#your-first-prompt}
|
||||
|
||||
```python title="server.py" hl_lines="6-9"
|
||||
--8<-- "docs_src/prompts/tutorial001.py"
|
||||
```
|
||||
|
||||
Le SDK lit les trois mêmes éléments qu’il lit sur un outil :
|
||||
|
||||
* Le **nom** est le nom de la fonction : `review_code`.
|
||||
* La **description** que le client affiche est la docstring : `Review a piece of code.`
|
||||
* Les **arguments** proviennent des paramètres. `code` n’a pas de valeur par défaut, il est donc obligatoire.
|
||||
|
||||
Voici ce qu’un client obtient en retour de `prompts/list` :
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "review_code",
|
||||
"description": "Review a piece of code.",
|
||||
"arguments": [
|
||||
{"name": "code", "required": true}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Il n’y a pas de JSON Schema ici. Les arguments d’un prompt forment une liste plate de **valeurs chaînes nommées** : un formulaire qu’une personne remplit, pas une charge utile qu’un modèle construit.
|
||||
|
||||
### Le rendre {#rendering-it}
|
||||
|
||||
Le client rend le modèle avec `prompts/get`, en passant les arguments. Votre fonction s’exécute et la `str` que vous renvoyez devient **un seul message utilisateur** :
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Review a piece of code.",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": {
|
||||
"type": "text",
|
||||
"text": "Please review this code:\n\ndef add(a, b): return a + b"
|
||||
}
|
||||
}
|
||||
],
|
||||
"resultType": "complete"
|
||||
}
|
||||
```
|
||||
|
||||
C’est toute la vie d’un prompt : listé par son nom, rendu à la demande, déposé dans la conversation.
|
||||
|
||||
!!! check
|
||||
`required` est vérifié avant l’exécution de votre fonction. Rendez `review_code` sans `code` et la
|
||||
requête elle-même échoue avec une erreur JSON-RPC (code `-32603`) :
|
||||
|
||||
```text
|
||||
mcp.shared.exceptions.MCPError: Internal server error
|
||||
```
|
||||
|
||||
Il n’y a pas de résultat d’erreur à la manière des outils à remettre à un modèle, car aucun modèle n’est dans la boucle :
|
||||
l’appel lève une exception. La raison (`Missing required arguments: {'code'}`) arrive dans le journal de votre serveur.
|
||||
|
||||
### Essayer {#try-it}
|
||||
|
||||
Lancez le serveur avec le MCP Inspector :
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Ouvrez l’onglet **Prompts** et sélectionnez `review_code`. L’Inspector dessine un formulaire avec un seul champ obligatoire `code`. Renseignez-le, lancez le rendu, et vous obtenez en retour exactement le message utilisateur ci-dessus.
|
||||
|
||||
## Plus d’un message {#more-than-one-message}
|
||||
|
||||
Une revue de code, c’est un message. Une session de débogage, c’est une conversation, et un prompt peut l’amorcer tout entière.
|
||||
|
||||
Renvoyez une liste de messages au lieu d’une `str` :
|
||||
|
||||
```python title="server.py" hl_lines="2 13-20"
|
||||
--8<-- "docs_src/prompts/tutorial002.py"
|
||||
```
|
||||
|
||||
* `UserMessage` et `AssistantMessage` viennent de `mcp.server.mcpserver.prompts.base`. Passez-leur une `str` et ils l’enveloppent dans un `TextContent` pour vous. Le rôle est le nom de la classe.
|
||||
* `Message` est leur classe de base commune. Utilisez-la comme annotation de retour.
|
||||
|
||||
Le rendu de `debug_error` produit désormais trois messages, dans l’ordre :
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Start a debugging conversation.",
|
||||
"messages": [
|
||||
{"role": "user", "content": {"type": "text", "text": "I'm seeing this error:"}},
|
||||
{"role": "user", "content": {"type": "text", "text": "TypeError: 'int' object is not iterable"}},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": {"type": "text", "text": "I'll help debug that. What have you tried so far?"}
|
||||
}
|
||||
],
|
||||
"resultType": "complete"
|
||||
}
|
||||
```
|
||||
|
||||
Remarquez le dernier. Préremplir un tour `assistant`, c’est la façon d’orienter la *prochaine* réponse du modèle sans obliger l’utilisateur à saisir lui-même cette orientation.
|
||||
|
||||
## Titres et descriptions d’arguments {#titles-and-argument-descriptions}
|
||||
|
||||
`review_code` est un nom de fonction, pas un libellé. Donnez au client quelque chose de mieux à afficher sur le bouton, et décrivez chaque argument pour que le formulaire s’explique de lui-même :
|
||||
|
||||
```python title="server.py" hl_lines="10-13"
|
||||
--8<-- "docs_src/prompts/tutorial003.py"
|
||||
```
|
||||
|
||||
* `title="Code review"` est le nom lisible par un humain, exactement comme le `title` d’un outil.
|
||||
* `Annotated[str, Field(description=...)]` est le même motif que celui que **[Outils](tools.md)** utilise pour décrire les paramètres d’un outil. Ici, la description se retrouve sur l’argument plutôt que dans un schéma.
|
||||
* `language` a une valeur par défaut, il cesse donc d’être obligatoire.
|
||||
|
||||
L’entrée `prompts/list` contient désormais tout ce dont un client a besoin pour dessiner un bon formulaire :
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "review_code",
|
||||
"title": "Code review",
|
||||
"description": "Review a piece of code.",
|
||||
"arguments": [
|
||||
{"name": "code", "description": "The code to review.", "required": true},
|
||||
{"name": "language", "description": "The language the code is written in.", "required": false}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
!!! info
|
||||
Si vous avez lu **[Outils](tools.md)**, vous connaissez déjà tout ce que contient cette page. Même décorateur, même
|
||||
docstring servant de description, mêmes `Annotated`/`Field`. Seuls changent qui
|
||||
le déclenche (l’utilisateur) et où va le résultat (dans la conversation).
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* `@mcp.prompt()` sur une fonction en fait un prompt. Le nom vient de la fonction, la description de la docstring.
|
||||
* Les prompts sont **contrôlés par l’utilisateur** : le client les liste, l’utilisateur en choisit un et renseigne les arguments.
|
||||
* Les arguments forment une liste plate de chaînes nommées (pas de schéma). Un paramètre avec une valeur par défaut est facultatif.
|
||||
* Renvoyez une `str` et elle devient un seul message utilisateur. Renvoyez une liste de `UserMessage` / `AssistantMessage` pour amorcer une conversation à plusieurs tours.
|
||||
* `title=` et `Field(description=...)` sont ce qu’un client affiche dans son interface.
|
||||
* Un argument obligatoire manquant fait échouer toute la requête. Il n’y a pas de résultat d’erreur par prompt.
|
||||
|
||||
L’autocomplétion côté serveur des arguments d’un prompt (ou d’un modèle de ressource), c’est **[Complétions](completions.md)**.
|
||||
146
i18n/fr/pages/servers/resources.md
Normal file
146
i18n/fr/pages/servers/resources.md
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
---
|
||||
translation:
|
||||
sections: [09df998c2a799f78, 0cf131146d16d4f9, 4e6b91e3f8025346, 8fe4eef576db17ed, 0d0d1ed43e3d0a53]
|
||||
tool: 1
|
||||
---
|
||||
# Ressources {#resources}
|
||||
|
||||
Une **ressource** (resource), ce sont des données que vous exposez pour que l’application les lise.
|
||||
|
||||
C’est là la ligne de partage. Un outil est quelque chose que le **modèle** décide d’appeler. Une ressource est quelque chose que l’**application** décide de charger (un fichier de configuration, un enregistrement, un document) et de placer devant le modèle comme contexte.
|
||||
|
||||
Vous en déclarez une en posant `@mcp.resource(uri)` sur une simple fonction Python.
|
||||
|
||||
## Votre première ressource {#your-first-resource}
|
||||
|
||||
```python title="server.py" hl_lines="6-8"
|
||||
--8<-- "docs_src/resources/tutorial001.py"
|
||||
```
|
||||
|
||||
C’est la même forme qu’un outil, avec une chose en plus : l’**URI**. Les ressources ont une adresse, pas un nom. Un client demande `config://app`, jamais `get_config`.
|
||||
|
||||
Le SDK lit tout de même le reste à partir de la fonction :
|
||||
|
||||
* Le **nom** est le nom de la fonction : `get_config`.
|
||||
* La **description** que voit le client est la docstring.
|
||||
* Le **contenu** est ce que vous renvoyez.
|
||||
|
||||
Lors de `resources/list`, le client reçoit ceci :
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "get_config",
|
||||
"uri": "config://app",
|
||||
"description": "The active shop configuration.",
|
||||
"mimeType": "text/plain"
|
||||
}
|
||||
```
|
||||
|
||||
Et lorsqu’il lit `config://app`, votre fonction s’exécute et la valeur de retour revient sous forme de texte :
|
||||
|
||||
```python
|
||||
result.contents # [TextResourceContents(uri="config://app", mime_type="text/plain", text="theme=dark\nlanguage=en")]
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Lister ne coûte rien. Votre fonction n’est **pas** appelée lors de `resources/list`, seulement lors
|
||||
de `resources/read`, et uniquement pour l’URI demandé. Exposez un millier de ressources
|
||||
et vous ne payez que pour celles que quelqu’un ouvre.
|
||||
|
||||
### Essayer {#try-it}
|
||||
|
||||
Lancez le serveur avec le MCP Inspector :
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Ouvrez l’URL qu’il affiche et allez dans l’onglet **Resources**. `config://app` figure dans la liste avec sa description. Cliquez dessus et l’Inspector la lit : voilà vos deux lignes de configuration.
|
||||
|
||||
## Modèles de ressources {#resource-templates}
|
||||
|
||||
Un URI par enregistrement, cela ne passe pas à l’échelle. Mettez un **paramètre de substitution** (placeholder) dans l’URI et un paramètre correspondant sur la fonction :
|
||||
|
||||
```python title="server.py" hl_lines="12-13"
|
||||
--8<-- "docs_src/resources/tutorial002.py"
|
||||
```
|
||||
|
||||
`{user_id}` dans l’URI, `user_id: str` sur la fonction. C’est tout le contrat.
|
||||
|
||||
Il s’agit désormais d’un **modèle de ressource** (resource template), et il déménage : il quitte `resources/list` et apparaît à la place dans `resources/templates/list`, sous forme de motif plutôt que d’adresse :
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "get_user_profile",
|
||||
"uriTemplate": "users://{user_id}/profile",
|
||||
"description": "A customer's profile.",
|
||||
"mimeType": "text/plain"
|
||||
}
|
||||
```
|
||||
|
||||
Le client remplit le paramètre de substitution et lit un URI concret : `users://42/profile`, `users://ada/profile`. Une seule fonction répond à tous, et reçoit la valeur extraite dans `user_id` :
|
||||
|
||||
```python
|
||||
result.contents # [TextResourceContents(uri="users://42/profile", text="User 42: 12 orders since 2021.")]
|
||||
```
|
||||
|
||||
Remarquez le champ `uri` dans le résultat. C’est l’URI **concret** demandé par le client, pas le modèle.
|
||||
|
||||
!!! check
|
||||
Les paramètres de substitution et les paramètres de la fonction doivent concorder. Renommez le
|
||||
paramètre de la fonction en `user` alors que l’URI dit toujours `{user_id}`, et le décorateur refuse
|
||||
**dès l’import**, avant qu’aucun client ne s’en approche :
|
||||
|
||||
```text
|
||||
ValueError: Mismatch between URI parameters {'user_id'} and function parameters {'user'}
|
||||
```
|
||||
|
||||
Une discordance ne peut être qu’un bug ; le SDK rend donc impossible le démarrage du serveur avec une telle erreur.
|
||||
|
||||
La syntaxe des paramètres de substitution est celle de la [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570) : `{+path}` pour les valeurs sur plusieurs segments, `{?q,lang}` pour les paramètres de requête optionnels, et bien d’autres. Par défaut, le SDK applique aussi des vérifications de sécurité des chemins aux valeurs extraites. Consultez **[Modèles d’URI et sécurité des chemins](uri-templates.md)** pour la référence complète.
|
||||
|
||||
`get_user_profile` peut également prendre un paramètre annoté `Context`. Le SDK l’injecte sans jamais le traiter comme un paramètre d’URI, et la page **[L’objet Context](../handlers/context.md)** décrit ce qu’il vous apporte.
|
||||
|
||||
## Ce que vous renvoyez {#what-you-return}
|
||||
|
||||
Vous n’êtes pas limité à `str`. Donnez à chaque ressource un `mime_type` et renvoyez ce qui convient :
|
||||
|
||||
```python title="server.py" hl_lines="8-9 14-15 20-21"
|
||||
--8<-- "docs_src/resources/tutorial003.py"
|
||||
```
|
||||
|
||||
* `readme` renvoie une `str`, elle est donc envoyée telle quelle. C’est le cas courant.
|
||||
* `catalog_stats` renvoie un `dict`, le SDK le sérialise donc pour vous en **texte JSON** :
|
||||
|
||||
```json
|
||||
{
|
||||
"books": 1204,
|
||||
"authors": 391
|
||||
}
|
||||
```
|
||||
|
||||
* `placeholder_cover` renvoie des `bytes`, le client reçoit donc un `BlobResourceContents` au lieu d’un `TextResourceContents`, avec vos octets encodés en base64 dans son champ `blob`.
|
||||
|
||||
La même règle vaut pour tout ce qui est sérialisable en JSON : une liste, un modèle Pydantic, une dataclass. Si ce n’est ni une `str` ni des `bytes`, cela devient du JSON.
|
||||
|
||||
C’est à vous de déclarer `mime_type`, et sa valeur par défaut est `text/plain`. Le SDK n’inspecte jamais ce que vous renvoyez pour le deviner : une ressource `dict` que vous n’étiquetez pas est donc toujours annoncée comme du texte brut.
|
||||
|
||||
!!! tip
|
||||
`@mcp.resource()` accepte aussi `name=`, `title=` et `description=` lorsque vous ne souhaitez
|
||||
pas les dériver de la fonction. Et lorsqu’il n’y a aucune fonction à écrire,
|
||||
`mcp.server.mcpserver.resources` propose des classes `Resource` prêtes à l’emploi (`TextResource`,
|
||||
`BinaryResource`, `FileResource`, `HttpResource`, `DirectoryResource`) que vous enregistrez
|
||||
avec `mcp.add_resource(...)`.
|
||||
|
||||
Un client peut aussi **s’abonner** à une ressource et être notifié lorsqu’elle change ; c’est la moitié de l’histoire côté client, et elle se trouve dans **[Le client](../client/index.md)**.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* `@mcp.resource(uri)` sur une fonction en fait une ressource. L’URI est l’adresse, la valeur de retour est le contenu, la docstring est la description.
|
||||
* Un `{placeholder}` dans l’URI en fait un **modèle** : il est listé sous `resources/templates/list` et une seule fonction sert tous les URI qui correspondent.
|
||||
* Les noms des paramètres de substitution doivent être identiques aux noms des paramètres de la fonction. Trompez-vous et vous le découvrez à l’import, pas en production.
|
||||
* Votre fonction s’exécute quand la ressource est **lue**, pas quand elle est listée.
|
||||
* `str` devient du texte, `bytes` devient un blob base64, tout le reste devient du texte JSON. `mime_type=` sert à l’étiqueter.
|
||||
* Les outils servent au modèle pour agir. Les ressources servent à l’application pour lire.
|
||||
|
||||
La troisième primitive, celle qu’une personne choisit dans un menu, ce sont les **[prompts](prompts.md)**.
|
||||
250
i18n/fr/pages/servers/structured-output.md
Normal file
250
i18n/fr/pages/servers/structured-output.md
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, e866c192e11d1c14, 6cdbad079f7b47f0, d4b607372fb28b51, 18dbf726ac45e0b7, c6f7d2a148aa49f4, c851964bb3301907, d715db6f8dccc9cc, ef86634aa70498a7]
|
||||
tool: 1
|
||||
---
|
||||
# Sortie structurée {#structured-output}
|
||||
|
||||
Un outil (tool) qui renvoie une simple `str` produit le résultat deux fois : sous forme de texte dans `content`, et sous la forme `{"result": "..."}` dans `structured_content`.
|
||||
|
||||
Cette page porte sur ce second canal : d’où il vient, toutes les formes qu’il peut prendre et la façon dont le SDK en garantit l’exactitude.
|
||||
|
||||
En bref : **l’annotation du type de retour est le schéma de sortie**. Vous l’avez déjà écrite.
|
||||
|
||||
## Le schéma de sortie {#the-output-schema}
|
||||
|
||||
```python title="server.py" hl_lines="9"
|
||||
--8<-- "docs_src/structured_output/tutorial001.py"
|
||||
```
|
||||
|
||||
La ligne qui compte est la signature : `-> int`.
|
||||
|
||||
Grâce à elle, l’outil que le SDK envoie lors de `tools/list` porte un `output_schema` à côté du schéma d’entrée qu’il construit à partir de vos paramètres (la page **[Outils](tools.md)** traite de celui-là) :
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"result": {"title": "Result", "type": "integer"}
|
||||
},
|
||||
"required": ["result"],
|
||||
"title": "get_temperatureOutput",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
Un `int` seul n’est pas un objet JSON, le SDK l’**enveloppe** donc dans `{"result": ...}`. Appelez l’outil et les deux canaux sont remplis :
|
||||
|
||||
```python
|
||||
result.content # [TextContent(text="17")]
|
||||
result.structured_content # {"result": 17}
|
||||
```
|
||||
|
||||
Tous les scalaires reçoivent la même enveloppe : `str`, `int`, `float`, `bool`, `bytes`, `None`.
|
||||
|
||||
## Deux canaux {#two-channels}
|
||||
|
||||
Pourquoi envoyer la même valeur deux fois ?
|
||||
|
||||
* `content` est destiné au **modèle**. Un modèle de langage lit du texte ; c’est la seule partie du résultat qu’il voit.
|
||||
* `structured_content` est destiné à l’**application** dans laquelle le modèle s’exécute : du code qui veut `17`, pas une phrase contenant « 17 ».
|
||||
* `output_schema` est le contrat entre les deux, publié avant même le premier appel de l’outil.
|
||||
|
||||
Vous renvoyez une seule valeur Python. Le SDK remplit les trois.
|
||||
|
||||
## Renvoyer un modèle {#return-a-model}
|
||||
|
||||
Déclarez la forme comme un `BaseModel` Pydantic et renvoyez une instance :
|
||||
|
||||
```python title="server.py" hl_lines="8-11 15"
|
||||
--8<-- "docs_src/structured_output/tutorial002.py"
|
||||
```
|
||||
|
||||
`WeatherData` **est** désormais le schéma. Pas d’enveloppe, pas de clé `result` :
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"temperature": {"description": "Degrees Celsius.", "title": "Temperature", "type": "number"},
|
||||
"humidity": {"description": "Relative humidity, 0 to 1.", "title": "Humidity", "type": "number"},
|
||||
"conditions": {"title": "Conditions", "type": "string"}
|
||||
},
|
||||
"required": ["temperature", "humidity", "conditions"],
|
||||
"title": "WeatherData",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
`structured_content` est l’objet, champ pour champ :
|
||||
|
||||
```python
|
||||
result.structured_content # {"temperature": 16.2, "humidity": 0.83, "conditions": "Overcast"}
|
||||
```
|
||||
|
||||
Et le modèle n’est pas oublié. Le SDK sérialise le même objet en texte JSON pour `content` :
|
||||
|
||||
```json
|
||||
{
|
||||
"temperature": 16.2,
|
||||
"humidity": 0.83,
|
||||
"conditions": "Overcast"
|
||||
}
|
||||
```
|
||||
|
||||
Remarquez que les `Field(description=...)` de `temperature` et `humidity` ont atterri dans le schéma. Le même `Field` qui décrivait vos **entrées** décrit vos sorties.
|
||||
|
||||
!!! info
|
||||
Si vous avez utilisé le `response_model` de FastAPI, vous connaissez déjà cela : un modèle Pydantic
|
||||
comme réponse déclarée, sérialisé et documenté pour vous. La seule différence est qu’ici
|
||||
l’annotation de retour constitue toute la déclaration.
|
||||
|
||||
## Un `TypedDict` {#a-typeddict}
|
||||
|
||||
Toutes les formes ne méritent pas une classe. Un `TypedDict` produit le même schéma :
|
||||
|
||||
```python title="server.py" hl_lines="8"
|
||||
--8<-- "docs_src/structured_output/tutorial003.py"
|
||||
```
|
||||
|
||||
Un `TypedDict` est un simple `dict` à l’exécution : c’est donc ce que vous construisez et renvoyez. Le schéma, la validation et `structured_content` sont identiques à la version `BaseModel` (à l’exception des descriptions, pour lesquelles `TypedDict` n’a pas de place).
|
||||
|
||||
## Une dataclass {#a-dataclass}
|
||||
|
||||
Les dataclasses fonctionnent aussi, tout comme n’importe quelle classe ordinaire dont les attributs portent des annotations de type. Le SDK construit en coulisses un modèle Pydantic à partir des annotations.
|
||||
|
||||
```python title="server.py" hl_lines="8-9"
|
||||
--8<-- "docs_src/structured_output/tutorial004.py"
|
||||
```
|
||||
|
||||
Trois écritures, un seul schéma. Utilisez celle que votre base de code emploie déjà.
|
||||
|
||||
## Listes {#lists}
|
||||
|
||||
Une `list[...]` n’est pas non plus un objet JSON : elle reçoit donc l’enveloppe `{"result": ...}`, avec votre type d’élément sous forme de référence `$defs` à l’intérieur :
|
||||
|
||||
```python title="server.py" hl_lines="15"
|
||||
--8<-- "docs_src/structured_output/tutorial005.py"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"$defs": {
|
||||
"WeatherData": {
|
||||
"properties": {
|
||||
"temperature": {"title": "Temperature", "type": "number"},
|
||||
"humidity": {"title": "Humidity", "type": "number"},
|
||||
"conditions": {"title": "Conditions", "type": "string"}
|
||||
},
|
||||
"required": ["temperature", "humidity", "conditions"],
|
||||
"title": "WeatherData",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"result": {"items": {"$ref": "#/$defs/WeatherData"}, "title": "Result", "type": "array"}
|
||||
},
|
||||
"required": ["result"],
|
||||
"title": "get_forecastOutput",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
Demandez une prévision sur deux jours et `structured_content` vaut `{"result": [{...}, {...}]}`. `content` devient **deux** blocs `TextContent`, un par élément : une liste est aplatie pour le modèle plutôt que déversée en une seule chaîne.
|
||||
|
||||
`tuple[...]`, les unions et `Optional[...]` sont enveloppés de la même façon.
|
||||
|
||||
## Dictionnaires {#dictionaries}
|
||||
|
||||
`dict[str, ...]` est le seul générique qui *est* déjà un objet JSON ; il n’est donc pas enveloppé :
|
||||
|
||||
```python title="server.py" hl_lines="9"
|
||||
--8<-- "docs_src/structured_output/tutorial006.py"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"additionalProperties": {"type": "number"},
|
||||
"title": "get_temperaturesDictOutput",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
result.structured_content # {"London": 16.2, "Reykjavik": 4.4}
|
||||
```
|
||||
|
||||
Les clés doivent être des `str`. Un `dict[int, float]` ne peut pas être un objet JSON ; il retombe donc sur l’enveloppe `{"result": ...}`.
|
||||
|
||||
## Validation {#validation}
|
||||
|
||||
`output_schema` n’est pas de la documentation. Tout ce que renvoie votre fonction est **validé par rapport à lui** avant de quitter le serveur.
|
||||
|
||||
Vous ne le remarquez pas tant que vous construisez la valeur à la main : Pydantic s’est déjà assuré que votre `WeatherData` était bien un `WeatherData`. Vous le remarquez le jour où les données viennent d’un endroit que vous ne contrôlez pas :
|
||||
|
||||
```python title="server.py" hl_lines="9 21"
|
||||
--8<-- "docs_src/structured_output/tutorial007.py"
|
||||
```
|
||||
|
||||
L’annotation promet un `WeatherData`. La réponse en amont a cessé d’envoyer `humidity`.
|
||||
|
||||
!!! check
|
||||
Appelez `get_weather` : il ne remet pas discrètement au client un objet à moitié vide. L’appel
|
||||
échoue, et les premières lignes de l’erreur nomment le champ :
|
||||
|
||||
```text
|
||||
Error executing tool get_weather: 1 validation error for WeatherData
|
||||
humidity
|
||||
Field required [type=missing, input_value={'temperature': 16.2, 'conditions': 'Overcast'}, input_type=dict]
|
||||
```
|
||||
|
||||
Ce texte revient comme résultat de l’outil avec `is_error=True` : le modèle sait donc que l’appel
|
||||
a échoué au lieu de lire avec assurance une météo qui n’existe pas.
|
||||
|
||||
Au passage, renvoyer un simple `dict` depuis un outil `-> WeatherData` ne pose aucun problème. C’est exactement ce que `json.loads` a produit. La validation porte sur la valeur, pas sur le type Python.
|
||||
|
||||
## Désactiver la sortie structurée {#opting-out}
|
||||
|
||||
Parfois, l’annotation de retour est destinée à votre vérificateur de types, pas au protocole. Passez `structured_output=False` et l’outil devient purement textuel :
|
||||
|
||||
```python title="server.py" hl_lines="6"
|
||||
--8<-- "docs_src/structured_output/tutorial008.py"
|
||||
```
|
||||
|
||||
Aucun `output_schema`, aucune enveloppe, aucune validation. `structured_content` vaut `None` et `content` est la chaîne que vous avez renvoyée.
|
||||
|
||||
L’inverse, `structured_output=True`, transforme la détection automatique en exigence : un outil dont le type de retour ne peut pas produire de schéma lève une exception à l’import au lieu de se rabattre sur du texte.
|
||||
|
||||
## Une classe sans annotations de type {#a-class-without-type-hints}
|
||||
|
||||
Il existe une façon de se retrouver sans sortie structurée sans l’avoir demandé : renvoyer une classe qui n’a **aucune annotation dans son corps**.
|
||||
|
||||
```python title="server.py" hl_lines="6-9"
|
||||
--8<-- "docs_src/structured_output/tutorial009.py"
|
||||
```
|
||||
|
||||
`Station` définit `name` et `online` dans `__init__`, mais la *classe* ne déclare rien. Le SDK lit les annotations de la classe, n’en trouve aucune et abandonne.
|
||||
|
||||
!!! warning
|
||||
Il abandonne **silencieusement**. `output_schema` vaut `None`, `structured_content` vaut `None`,
|
||||
et le texte que lit le modèle est le `repr` de l’objet :
|
||||
|
||||
```text
|
||||
"<server.Station object at 0x7f539d75b230>"
|
||||
```
|
||||
|
||||
Aucune erreur, aucun avertissement, un outil inutile. Déplacez les annotations dans le corps de la
|
||||
classe, ou passez `structured_output=True`, qui transforme cela en erreur franche dès l’import du
|
||||
module : `Function get_station: return type <class 'server.Station'> is not serializable for structured output`.
|
||||
|
||||
!!! tip
|
||||
Besoin d’un contrôle total (construire vous-même le `CallToolResult`, ou attacher un `_meta` que
|
||||
l’application voit mais pas le modèle) ? C’est le sujet de **[Le Server de bas niveau](../advanced/low-level-server.md)**.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* L’**annotation du type de retour** est le schéma de sortie. Elle est publiée dans `tools/list` sous le nom `output_schema`.
|
||||
* Les scalaires, listes, tuples et unions sont enveloppés dans `{"result": ...}`. Les modèles, les `TypedDict`, les dataclasses, les classes annotées et `dict[str, ...]` sont déjà des objets et restent tels quels.
|
||||
* Chaque résultat porte `content` (du texte, pour le modèle) **et** `structured_content` (des données, pour l’application).
|
||||
* Ce que vous renvoyez est validé par rapport au schéma. Une incohérence est une erreur d’outil, pas un résultat corrompu.
|
||||
* `structured_output=False` désactive la sortie structurée d’un outil. Une classe sans annotations de type la désactive silencieusement ; surveillez ce cas.
|
||||
|
||||
Vous maîtrisez désormais tout ce qu’un outil peut répondre. Ensuite, la deuxième primitive : **[Ressources](resources.md)**.
|
||||
177
i18n/fr/pages/servers/tools.md
Normal file
177
i18n/fr/pages/servers/tools.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
---
|
||||
translation:
|
||||
sections: [e4cc390d56573409, 8566e2b68594e9ad, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, edbedf2a16e71311, 3d8ef8da89fa87c1, f6c0e02e6ea5a363]
|
||||
tool: 1
|
||||
---
|
||||
# Outils {#tools}
|
||||
|
||||
Un **outil** (tool) est une fonction que le modèle peut appeler.
|
||||
|
||||
Vous en déclarez un en posant `@mcp.tool()` sur une simple fonction Python. C’est toute l’API.
|
||||
|
||||
## Votre premier outil {#your-first-tool}
|
||||
|
||||
```python title="server.py" hl_lines="6-8"
|
||||
--8<-- "docs_src/tools/tutorial001.py"
|
||||
```
|
||||
|
||||
Regardez ce que vous avez écrit. Pas de schémas, pas de JSON, pas de protocole : juste une fonction. Le SDK en lit trois choses :
|
||||
|
||||
* Le **nom** de l’outil est le nom de la fonction : `search_books`.
|
||||
* La **description** que voit le modèle est la docstring : `Search the catalog by title or author.`
|
||||
* Les **arguments** que le modèle a le droit de passer proviennent des annotations de type : `query: str` et `limit: int`.
|
||||
|
||||
### Le schéma d’entrée {#the-input-schema}
|
||||
|
||||
À partir de ces annotations de type, le SDK génère un JSON Schema et l’envoie au client lors de `tools/list` :
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"title": "Query", "type": "string"},
|
||||
"limit": {"title": "Limit", "type": "integer"}
|
||||
},
|
||||
"required": ["query", "limit"],
|
||||
"title": "search_booksArguments"
|
||||
}
|
||||
```
|
||||
|
||||
Les deux arguments figurent dans `required` parce qu’aucun n’a de valeur par défaut. Vous allez corriger cela dans un instant. (Les clés `title` sont des artefacts de Pydantic ; les propriétés, leurs types et `required` constituent le contrat.)
|
||||
|
||||
!!! tip
|
||||
Ici, les annotations de type ne sont pas de la documentation. Elles sont **le contrat**. Si un client envoie `"limit": "ten"`,
|
||||
le SDK le rejette avant même que votre fonction ne s’exécute.
|
||||
|
||||
### Ce que le modèle reçoit en retour {#what-the-model-gets-back}
|
||||
|
||||
Appelez l’outil avec `{"query": "dune", "limit": 5}` et le résultat comporte deux parties :
|
||||
|
||||
```python
|
||||
result.content # [TextContent(text="Found 3 books matching 'dune' (showing up to 5).")]
|
||||
result.structured_content # {'result': "Found 3 books matching 'dune' (showing up to 5)."}
|
||||
```
|
||||
|
||||
`content` est le texte que lit le **modèle**. `structured_content` contient des données typées destinées à l’**application cliente**. Elles sont là parce que vous avez déclaré le type de retour `-> str`.
|
||||
|
||||
Ne vous souciez pas encore de `structured_content`. Renvoyez de vrais objets Python depuis vos outils et tout se passe comme il faut ; la page **[Sortie structurée](structured-output.md)** y est entièrement consacrée.
|
||||
|
||||
### Essayer {#try-it}
|
||||
|
||||
Lancez le serveur avec le MCP Inspector :
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Ouvrez l’URL qu’il affiche, allez dans l’onglet **Tools** et appelez `search_books`.
|
||||
|
||||
L’Inspector affiche un formulaire avec un champ texte `query` obligatoire et un champ numérique `limit` obligatoire. Il a construit ce formulaire à partir de vos annotations de type. Tous les autres clients MCP feront de même.
|
||||
|
||||
## Arguments optionnels {#optional-arguments}
|
||||
|
||||
Donnez une valeur par défaut à un paramètre et il cesse d’être obligatoire. C’est tout. C’est du Python, tout simplement.
|
||||
|
||||
```python title="server.py" hl_lines="7"
|
||||
--8<-- "docs_src/tools/tutorial002.py"
|
||||
```
|
||||
|
||||
Le schéma suit :
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"title": "Query", "type": "string"},
|
||||
"limit": {"default": 10, "title": "Limit", "type": "integer"}
|
||||
},
|
||||
"required": ["query"],
|
||||
"title": "search_booksArguments"
|
||||
}
|
||||
```
|
||||
|
||||
`limit` a quitté `required` et a gagné `"default": 10`. Un client qui l’omet obtient `10`, exactement comme en Python.
|
||||
|
||||
## Des schémas plus riches avec `Field` {#richer-schemas-with-field}
|
||||
|
||||
Les annotations de type vous mènent loin, mais vous voulez parfois *décrire* un argument, ou le contraindre.
|
||||
|
||||
Enveloppez le type dans `Annotated` et ajoutez un `Field` Pydantic :
|
||||
|
||||
```python title="server.py" hl_lines="12-14"
|
||||
--8<-- "docs_src/tools/tutorial003.py"
|
||||
```
|
||||
|
||||
Trois nouveautés, toutes sur les paramètres :
|
||||
|
||||
* `Field(description=...)` : une description par argument, que le modèle lit en plus de la docstring.
|
||||
* `Field(ge=1, le=50)` : des bornes numériques. Elles arrivent dans le schéma sous la forme `"minimum": 1, "maximum": 50`.
|
||||
* `Literal["fiction", "non-fiction", "poetry"]` : une énumération. Le modèle ne peut choisir que l’une de ces valeurs.
|
||||
|
||||
!!! check
|
||||
Les contraintes ne sont pas décoratives. Appelez l’outil avec `limit=999` et le SDK répond par une
|
||||
erreur d’outil **avant que votre fonction ne s’exécute** :
|
||||
|
||||
```text
|
||||
Input should be less than or equal to 50
|
||||
```
|
||||
|
||||
Cette erreur revient au modèle comme résultat de l’outil ; le modèle la lit et réessaie avec
|
||||
une valeur valide. Vous avez écrit `le=50` une seule fois et obtenu, sans rien de plus, des agents qui se corrigent d’eux-mêmes.
|
||||
|
||||
!!! info
|
||||
Si vous avez utilisé FastAPI ou Pydantic, vous connaissez déjà tout cela. C’est le même `Field`,
|
||||
le même `Annotated`, la même validation. Il n’y a rien de propre à MCP à apprendre ici.
|
||||
|
||||
## Un modèle comme paramètre {#a-model-as-a-parameter}
|
||||
|
||||
Quand un outil prend plus de deux ou trois arguments, regroupez-les dans un modèle Pydantic :
|
||||
|
||||
```python title="server.py" hl_lines="8-11 15"
|
||||
--8<-- "docs_src/tools/tutorial004.py"
|
||||
```
|
||||
|
||||
Le schéma de `Book` est imbriqué dans le schéma d’entrée de l’outil (sous forme de référence `$defs`), le modèle le remplit comme un objet JSON, et votre fonction reçoit une **véritable instance de `Book`**, déjà validée, avec les attributs `.title`, `.author` et `.year`.
|
||||
|
||||
Vous pouvez combiner librement : des paramètres simples à côté de paramètres modèles, des modèles imbriqués, des listes de modèles. C’est du Pydantic de bout en bout.
|
||||
|
||||
## `async def` {#async-def}
|
||||
|
||||
Si un outil fait des E/S (appelle une API, lit un fichier, interroge une base de données), déclarez-le en `async def` et utilisez `await` à l’intérieur. Le SDK se charge de l’attendre.
|
||||
|
||||
Un outil en simple `def` fonctionne aussi : le SDK l’exécute dans un thread, si bien qu’il ne bloque jamais le serveur.
|
||||
|
||||
Il n’y a rien d’autre à configurer.
|
||||
|
||||
## Noms, titres et annotations {#names-titles-and-annotations}
|
||||
|
||||
Tout ce que le SDK déduit, vous pouvez le redéfinir dans le décorateur :
|
||||
|
||||
```python title="server.py" hl_lines="7-10"
|
||||
--8<-- "docs_src/tools/tutorial005.py"
|
||||
```
|
||||
|
||||
* `title` est un nom lisible par un humain, destiné aux interfaces. Les clients affichent *« Search the catalog »* au lieu de `search_books`.
|
||||
* `annotations` regroupe des **indications** de comportement destinées au client :
|
||||
* `read_only_hint=True` : cet outil ne modifie rien.
|
||||
* `open_world_hint=False` : il opère sur un ensemble fermé de choses (ce catalogue), pas sur le web ouvert.
|
||||
* Les deux autres, `destructive_hint` et `idempotent_hint`, décrivent un outil qui *écrit* : peut-il
|
||||
supprimer quelque chose, et l’appeler deux fois revient-il au même que l’appeler une fois ? La spécification ne les définit
|
||||
que pour les outils qui ne sont pas en lecture seule ; elles ne diraient donc rien sur `search_books`.
|
||||
|
||||
Un client bien conçu s’en sert pour trancher des questions comme *« dois-je demander à l’utilisateur avant d’exécuter ceci ? »*. Ce sont des indications, pas de la sécurité. Ne comptez jamais sur un client pour les respecter.
|
||||
|
||||
!!! tip
|
||||
`name=` et `description=` sont également acceptés par `@mcp.tool()` si vous ne voulez pas les dériver
|
||||
du nom de la fonction et de la docstring. La plupart du temps, c’est ce que vous voulez.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* `@mcp.tool()` sur une fonction en fait un outil. Le nom vient de la fonction, la description de la docstring.
|
||||
* Les annotations de type **sont** le schéma d’entrée. Les valeurs par défaut rendent les arguments optionnels.
|
||||
* `Annotated[..., Field(...)]` ajoute descriptions et contraintes ; `Literal` ajoute les énumérations.
|
||||
* Un paramètre modèle Pydantic est la façon de recevoir un « corps » structuré.
|
||||
* Les arguments invalides sont rejetés pour vous, avec une erreur que le modèle peut lire et dont il peut se remettre.
|
||||
* `async def` pour les E/S, `def` tout court pour tout le reste.
|
||||
|
||||
**[Sortie structurée](structured-output.md)** explique ce qu’il advient de la valeur que vous renvoyez avec `return`.
|
||||
303
i18n/fr/pages/servers/uri-templates.md
Normal file
303
i18n/fr/pages/servers/uri-templates.md
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
---
|
||||
translation:
|
||||
sections: [4a7033e1ed8ad602, 55dcbfff0c6271bf, 101ef9d14bf4ec46, 4b6c4a845438abc7, f98b46bafbee4acd]
|
||||
tool: 1
|
||||
---
|
||||
# Modèles d’URI et sûreté des chemins {#uri-templates-and-path-safety}
|
||||
|
||||
Cette page est la référence de la syntaxe de modèle d’URI (URI template)
|
||||
qu’accepte [`@mcp.resource`](resources.md), ainsi que de la politique de
|
||||
sûreté des chemins que le SDK applique aux valeurs extraites. Pour une
|
||||
introduction à ce que sont les ressources et au moment où les utiliser,
|
||||
commencez par **[Ressources](resources.md)** ; cette page suppose que vous
|
||||
savez déjà déclarer une ressource et que vous cherchez le jeu complet
|
||||
d’opérateurs, les réglages de sécurité ou le câblage de bas niveau.
|
||||
|
||||
La syntaxe des modèles est celle de la [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570).
|
||||
Le SDK en prend en charge un sous-ensemble choisi pour faire correspondre
|
||||
les URI des requêtes `resources/read` entrantes, auquel s’ajoute une couche
|
||||
de sécurité qui rejette les valeurs qui se résoudraient en dehors du
|
||||
répertoire que vous comptez servir. Pour les détails au niveau du protocole
|
||||
(formats des messages, cycle de vie, pagination), consultez la
|
||||
[spécification MCP des ressources](https://modelcontextprotocol.io/specification/latest/server/resources).
|
||||
|
||||
## Le jeu complet d’opérateurs {#the-full-operator-set}
|
||||
|
||||
L’espace réservé simple, `{user_id}`, est celui que présente **[Ressources](resources.md)**. Il existe quatre autres
|
||||
formes d’opérateur ; les voici réunies sur un même serveur pour que vous
|
||||
puissiez les comparer côte à côte :
|
||||
|
||||
```python title="server.py" hl_lines="16-17 22-23 28-29 34-35 40-41"
|
||||
--8<-- "docs_src/uri_templates/tutorial001.py"
|
||||
```
|
||||
|
||||
Chaque décorateur mis en évidence découpe l’URI d’une manière différente.
|
||||
Les sections ci-dessous les parcourent de haut en bas.
|
||||
|
||||
### Expansion simple : `{name}` {#simple-expansion-name}
|
||||
|
||||
`books://{isbn}` est la forme simple, celle de tous les jours. L’espace
|
||||
réservé correspond au paramètre `isbn` ; un client qui lit
|
||||
`books://978-0441172719` appelle donc `get_book("978-0441172719")`.
|
||||
|
||||
Un `{name}` simple s’arrête au premier `/`. `books://978/extra` ne
|
||||
correspond pas, car la barre oblique après `978` met fin à la capture et
|
||||
`/extra` reste en trop.
|
||||
|
||||
### Conversion de type {#type-conversion}
|
||||
|
||||
Les valeurs extraites arrivent sous forme de chaînes, mais vous pouvez
|
||||
déclarer un type plus précis et le SDK se charge de la conversion.
|
||||
`orders://{order_id}` aboutit dans une fonction dont le paramètre est
|
||||
`order_id: int` ; lire `orders://12345` appelle donc `get_order(12345)`, et
|
||||
non `get_order("12345")`. Le gestionnaire (handler) fait de l’arithmétique
|
||||
dessus (`order_id + 1`) sans transtypage.
|
||||
|
||||
### Chemins à plusieurs segments : `{+name}` {#multi-segment-paths-name}
|
||||
|
||||
Pour capturer une valeur qui contient des barres obliques, utilisez
|
||||
`{+name}`. Avec `manuals://{+path}` :
|
||||
|
||||
* `manuals://returns.md` donne `path = "returns.md"`
|
||||
* `manuals://printing/setup.md` donne `path = "printing/setup.md"`
|
||||
|
||||
Tournez-vous vers `{+name}` dès que la valeur est hiérarchique : chemins
|
||||
du système de fichiers, clés d’objets imbriqués, chemins d’URL que vous
|
||||
relayez.
|
||||
|
||||
### Paramètres de requête : `{?a,b,c}` {#query-parameters-abc}
|
||||
|
||||
`reviews://{isbn}{?limit,sort}` place `limit` et `sort` après le `?`.
|
||||
Le chemin identifie *quel* livre ; la chaîne de requête règle *comment*
|
||||
vous le lisez.
|
||||
|
||||
Les paramètres de requête sont mis en correspondance avec souplesse :
|
||||
l’ordre n’a pas d’importance, les paramètres en trop sont ignorés et les
|
||||
paramètres omis retombent sur les valeurs par défaut de votre fonction.
|
||||
Ainsi, `reviews://978-0441172719` utilise `limit=10, sort="newest"`, et
|
||||
`reviews://978-0441172719?sort=top` ne remplace que `sort`.
|
||||
|
||||
### Segments de chemin sous forme de liste : `{/name*}` {#path-segments-as-a-list-name}
|
||||
|
||||
Si vous voulez chaque segment de chemin comme un élément de liste distinct
|
||||
plutôt qu’une seule chaîne contenant des barres obliques, utilisez
|
||||
`{/name*}`. Avec `shelves://browse{/path*}`, un client qui lit
|
||||
`shelves://browse/fiction/sci-fi` appelle
|
||||
`browse_shelf(["fiction", "sci-fi"])`.
|
||||
|
||||
### Référence des modèles {#template-reference}
|
||||
|
||||
Les motifs les plus courants :
|
||||
|
||||
| Motif | Exemple d’entrée | Vous obtenez |
|
||||
|--------------|-----------------------|-------------------------|
|
||||
| `{name}` | `alice` | `"alice"` |
|
||||
| `{name}` | `docs/intro.md` | *pas de correspondance* (s’arrête au `/`) |
|
||||
| `{+path}` | `docs/intro.md` | `"docs/intro.md"` |
|
||||
| `{.ext}` | `.json` | `"json"` |
|
||||
| `{/segment}` | `/v2` | `"v2"` |
|
||||
| `{?key}` | `?key=value` | `"value"` |
|
||||
| `{?a,b}` | `?a=1&b=2` | `"1"`, `"2"` |
|
||||
| `{/path*}` | `/a/b/c` | `["a", "b", "c"]` |
|
||||
|
||||
### Ce que l’analyseur rejette {#what-the-parser-rejects}
|
||||
|
||||
Quelques formes de modèle sont interceptées d’emblée plutôt que d’échouer
|
||||
à la première requête. `@mcp.resource` analyse le modèle au moment où le
|
||||
décorateur s’exécute ; aucune d’entre elles n’atteint donc jamais un
|
||||
serveur en fonctionnement.
|
||||
|
||||
`UriTemplate.parse()` lève `InvalidUriTemplate` pour :
|
||||
|
||||
* **Deux variables sans rien entre elles.** `manuals://{+path}{ext}`
|
||||
est rejeté : la mise en correspondance ne peut pas savoir où `path` se
|
||||
termine et où `ext` commence. Placez un littéral entre les deux
|
||||
(`manuals://{+path}/{ext}`) ou utilisez un opérateur qui fournit son
|
||||
propre délimiteur. `manuals://{+path}{.ext}` est accepté parce que
|
||||
`{.ext}` apporte lui-même le `.`.
|
||||
* **Plus d’une variable à plusieurs segments.** Au plus une variable
|
||||
parmi `{+var}`, `{#var}` ou une variable éclatée (`{/var*}`, `{.var*}`,
|
||||
`{;var*}`) par modèle. Deux sont intrinsèquement ambiguës : il n’existe
|
||||
aucun moyen rigoureux de décider laquelle absorbe un segment
|
||||
supplémentaire.
|
||||
* **Les erreurs de syntaxe habituelles** : une accolade non fermée, un nom
|
||||
de variable utilisé deux fois ou une fonctionnalité de la RFC 6570 que
|
||||
le SDK ne prend pas en charge, comme le modificateur de préfixe
|
||||
`{var:3}` ou l’éclatement de requête `{?vars*}`.
|
||||
|
||||
En plus de cela, `@mcp.resource` lève `ValueError` lorsqu’un paramètre du
|
||||
gestionnaire est lié à une variable de requête dans la séquence finale
|
||||
`{?...}`/`{&...}` du modèle mais n’a pas de valeur par défaut Python. Ces
|
||||
variables sont mises en correspondance avec souplesse (un client peut
|
||||
omettre n’importe laquelle), si bien qu’un paramètre sans valeur par défaut
|
||||
ne se manifesterait que sous la forme d’une erreur interne opaque à la
|
||||
première requête qui l’omet. `reviews://{isbn}{?limit,sort}` dans le
|
||||
serveur ci-dessus est la version bien formée : `limit` et `sort` portent
|
||||
tous deux une valeur par défaut.
|
||||
|
||||
## Sécurité {#security}
|
||||
|
||||
Les paramètres de modèle proviennent du client. S’ils se retrouvent sans
|
||||
contrôle dans des opérations sur le système de fichiers ou la base de
|
||||
données, des valeurs comme `../../etc/passwd` peuvent se résoudre en
|
||||
dehors du répertoire que vous comptiez servir.
|
||||
|
||||
### Ce que le SDK vérifie par défaut {#what-the-sdk-checks-by-default}
|
||||
|
||||
Avant que votre gestionnaire ne s’exécute, le SDK rejette tout paramètre
|
||||
qui :
|
||||
|
||||
* s’échapperait de son répertoire de départ via des composants `..`
|
||||
* ressemble à un chemin absolu (`/etc/passwd`, `C:\Windows`) ou à un
|
||||
chemin Windows relatif à un lecteur (`C:foo`). Une valeur relative à un
|
||||
lecteur et un identifiant à espace de noms comme `x:y` sont
|
||||
indiscernables en tant que chaînes ; toute valeur composée d’une seule
|
||||
lettre suivie de deux-points est donc rejetée par défaut. Exemptez le
|
||||
paramètre s’il reçoit légitimement de telles valeurs
|
||||
* contient un octet nul (`\x00`)
|
||||
|
||||
La vérification des `..` se fait par composant, et non par recherche de
|
||||
sous-chaîne. Des valeurs comme `v1.0..v2.0` ou `HEAD~3..HEAD` passent,
|
||||
car `..` n’y constitue pas un segment de chemin autonome.
|
||||
|
||||
Ces vérifications s’appliquent à la valeur décodée ; elles interceptent
|
||||
donc la traversée de répertoires quelle que soit la façon dont elle a été
|
||||
encodée dans l’URI (`../etc`, `..%2Fetc`, `%2E%2E/etc`, `..%5Cetc`, `%00`
|
||||
sont tous interceptés).
|
||||
|
||||
!!! check
|
||||
Lisez `manuals://../etc/passwd` sur le serveur ci-dessus et la requête
|
||||
est rejetée purement et simplement : la mise en correspondance des
|
||||
modèles s’arrête au premier échec, si bien qu’aucun modèle ultérieur
|
||||
(potentiellement plus permissif) n’est essayé en repli. Le client voit
|
||||
la même erreur `-32602` « Unknown resource » que pour un URI qui ne
|
||||
correspond à aucun modèle, et `read_manual` ne s’exécute jamais.
|
||||
|
||||
### Gestionnaires sur le système de fichiers : utiliser safe_join {#filesystem-handlers-use-safe_join}
|
||||
|
||||
Les vérifications intégrées bloquent les cas courants, mais ne peuvent pas
|
||||
connaître la frontière de votre bac à sable. Pour l’accès au système de
|
||||
fichiers, utilisez `safe_join` pour résoudre le chemin et vérifier qu’il
|
||||
reste à l’intérieur de votre répertoire de base :
|
||||
|
||||
```python title="server.py" hl_lines="4 14"
|
||||
--8<-- "docs_src/uri_templates/tutorial002.py"
|
||||
```
|
||||
|
||||
`safe_join` intercepte les échappements par lien symbolique, les séquences
|
||||
`..` et les astuces à base de chemin absolu qu’une simple vérification de
|
||||
chaîne laisserait passer. Si le chemin résolu s’échappe de `DOCS_ROOT`, il
|
||||
lève `PathEscapeError`, qui parvient au client sous la forme d’une
|
||||
`ResourceError`.
|
||||
|
||||
### Quand les valeurs par défaut vous gênent {#when-the-defaults-get-in-the-way}
|
||||
|
||||
Parfois, les vérifications bloquent des valeurs légitimes. Un outil
|
||||
d’importation de catalogue peut recevoir intentionnellement un chemin
|
||||
absolu, ou un paramètre peut être une référence relative comme
|
||||
`../sibling` que votre gestionnaire interprète en toute sécurité sans
|
||||
toucher au système de fichiers. Exemptez ce paramètre ou assouplissez la
|
||||
politique pour tout le serveur :
|
||||
|
||||
```python title="server.py" hl_lines="9 16-19"
|
||||
--8<-- "docs_src/uri_templates/tutorial003.py"
|
||||
```
|
||||
|
||||
* `security=ResourceSecurity(exempt_params={"source"})` sur le décorateur
|
||||
saute les vérifications pour ce seul paramètre sur cette seule
|
||||
ressource. Le reste du serveur conserve la politique par défaut.
|
||||
* `resource_security=` sur le constructeur de `MCPServer` définit la
|
||||
valeur par défaut pour chaque ressource. Ici, `relaxed` désactive
|
||||
entièrement la vérification des `..`.
|
||||
|
||||
Les vérifications configurables :
|
||||
|
||||
| Réglage | Par défaut | Ce qu’il fait |
|
||||
|-------------------------|---------|-------------------------------------|
|
||||
| `reject_path_traversal` | `True` | Rejette les séquences `..` qui s’échappent du répertoire de départ |
|
||||
| `reject_absolute_paths` | `True` | Rejette `/foo`, `C:\foo`, les chemins UNC et le `C:foo` relatif à un lecteur (intercepte aussi `x:y`) |
|
||||
| `reject_null_bytes` | `True` | Rejette les valeurs contenant `\x00` |
|
||||
| `exempt_params` | vide | Noms des paramètres à exempter des vérifications |
|
||||
|
||||
Ces vérifications sont un préfiltre heuristique ; pour l’accès au système
|
||||
de fichiers, `safe_join` reste la frontière de confinement.
|
||||
|
||||
!!! tip
|
||||
Si votre gestionnaire ne peut pas satisfaire la requête (le fichier
|
||||
n’existe pas, l’identifiant est inconnu), levez une exception. Le SDK
|
||||
la transforme en réponse d’erreur. Consultez **[Gérer les erreurs](handling-errors.md)** pour la
|
||||
différence entre une erreur de protocole et une erreur d’outil.
|
||||
|
||||
## Les ressources sur le Server de bas niveau {#resources-on-the-low-level-server}
|
||||
|
||||
Si vous construisez sur le `Server` de bas niveau (voir **[Le Server de
|
||||
bas niveau](../advanced/low-level-server.md)**), vous enregistrez directement des gestionnaires pour les
|
||||
méthodes de protocole `resources/list` et `resources/read`. Il n’y a pas
|
||||
de décorateur ; vous renvoyez vous-même les types du protocole.
|
||||
|
||||
### Ressources statiques {#static-resources}
|
||||
|
||||
Pour des URI fixes, tenez un registre et répartissez sur correspondance
|
||||
exacte :
|
||||
|
||||
```python title="server.py" hl_lines="17 21 27"
|
||||
--8<-- "docs_src/uri_templates/tutorial004.py"
|
||||
```
|
||||
|
||||
Le gestionnaire de liste indique aux clients ce qui est disponible ; le
|
||||
gestionnaire de lecture sert le contenu. Consultez d’abord votre registre,
|
||||
retombez sur les modèles (ci-dessous) si vous en avez, puis levez une
|
||||
exception pour tout le reste.
|
||||
|
||||
### Modèles {#templates}
|
||||
|
||||
Le moteur de modèles qu’utilise `MCPServer` se trouve dans
|
||||
`mcp.shared.uri_template` et fonctionne de manière autonome. Vous
|
||||
bénéficiez de la même analyse et de la même mise en correspondance ; vous
|
||||
câblez vous-même le routage et la politique de sécurité.
|
||||
|
||||
```python title="server.py" hl_lines="13-16 22-25 29 33 45"
|
||||
--8<-- "docs_src/uri_templates/tutorial005.py"
|
||||
```
|
||||
|
||||
Trois choses se passent dans les lignes mises en évidence :
|
||||
|
||||
* **Analyser une fois, faire correspondre à chaque requête.**
|
||||
`UriTemplate.parse()` construit le modèle ; `template.match(uri)`
|
||||
renvoie les variables extraites sous forme de `dict`, ou `None` si l’URI
|
||||
ne convient pas. Le décodage d’URL a lieu dans `match()` ; les valeurs
|
||||
décodées sont renvoyées telles quelles, sans validation de sûreté des
|
||||
chemins. Les valeurs sortent sous forme de chaînes : convertissez-les
|
||||
vous-même (`int(matched["id"])`, `Path(matched["path"])`).
|
||||
* **Appliquer vous-même les vérifications de sûreté.** Les vérifications
|
||||
des `..` et des chemins absolus que `MCPServer` exécute par défaut se
|
||||
trouvent dans `mcp.shared.path_security`. `read_manual_safely` les
|
||||
appelle avant de toucher à `MANUALS`. Si un paramètre n’est pas un
|
||||
chemin du système de fichiers (un ISBN, une requête de recherche),
|
||||
sautez les vérifications pour cette valeur : vous maîtrisez la politique
|
||||
gestionnaire par gestionnaire plutôt qu’au travers d’un objet de
|
||||
configuration.
|
||||
* **Lister les modèles à partir de la même source.** Les clients
|
||||
découvrent les modèles via `resources/templates/list`. `str(template)`
|
||||
restitue la chaîne de modèle d’origine, si bien que la liste et le
|
||||
moteur de correspondance partagent une seule source de vérité.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* `{name}` correspond à un seul segment ; `{+name}` conserve les barres
|
||||
obliques ; `{?a,b}` puise dans la chaîne de requête ; `{/name*}` découpe
|
||||
les segments en liste.
|
||||
* Deux variables sans rien entre elles, ou une seconde variable à
|
||||
plusieurs segments, sont rejetées à l’analyse. Un paramètre lié à une
|
||||
variable de requête dans une séquence finale `{?...}`/`{&...}` doit
|
||||
déclarer une valeur par défaut Python.
|
||||
* Annotez le paramètre (`order_id: int`) et le SDK convertit.
|
||||
* La politique de sécurité par défaut rejette `..`, les chemins absolus
|
||||
et les octets nuls avant que votre gestionnaire ne s’exécute ;
|
||||
remplacez-la par ressource avec `security=ResourceSecurity(...)` ou pour
|
||||
tout le serveur avec `resource_security=`.
|
||||
* Pour l’accès au système de fichiers, `safe_join` est la frontière de
|
||||
confinement.
|
||||
* Sur le `Server` de bas niveau, analysez avec `UriTemplate.parse()`,
|
||||
faites correspondre avec `.match()` et appliquez
|
||||
`mcp.shared.path_security` vous-même.
|
||||
Loading…
Reference in a new issue