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
145
i18n/fr/pages/run/asgi.md
Normal file
145
i18n/fr/pages/run/asgi.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
---
|
||||
translation:
|
||||
sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3]
|
||||
tool: 1
|
||||
---
|
||||
# Ajouter à une application existante {#add-to-an-existing-app}
|
||||
|
||||
`mcp.run("streamable-http")` démarre un serveur web pour vous. Parfois, ce n’est pas ce que vous voulez : votre serveur MCP n’est qu’une pièce d’une application web plus vaste, ou vous avez déjà un déploiement ASGI.
|
||||
|
||||
Pour cela, `mcp.streamable_http_app()` renvoie une **application Starlette**.
|
||||
|
||||
Une application Starlette est une application ASGI, donc tout ce qui héberge de l’ASGI (uvicorn, Hypercorn, une autre application Starlette, FastAPI) peut héberger votre serveur MCP.
|
||||
|
||||
## L’application {#the-app}
|
||||
|
||||
```python title="server.py" hl_lines="12"
|
||||
--8<-- "docs_src/asgi/tutorial001.py"
|
||||
```
|
||||
|
||||
`app` est une application ASGI ordinaire. Passez-la à n’importe quel serveur ASGI :
|
||||
|
||||
```console
|
||||
uvicorn server:app
|
||||
```
|
||||
|
||||
Le point de terminaison MCP se trouve à `/mcp`, un client se connecte donc à `http://127.0.0.1:8000/mcp`.
|
||||
|
||||
L’application embarque déjà deux choses :
|
||||
|
||||
* Une route, `/mcp` : le point de terminaison Streamable HTTP.
|
||||
* Un **cycle de vie** (lifespan) qui démarre `mcp.session_manager`, l’objet responsable du travail d’arrière-plan de chaque session active.
|
||||
|
||||
Exécutez l’application seule (`uvicorn server:app`) et vous n’aurez jamais à penser ni à l’un ni à l’autre.
|
||||
|
||||
!!! tip
|
||||
`streamable_http_app()` accepte les mêmes arguments nommés que `mcp.run("streamable-http", ...)`,
|
||||
à l’exception de `port` : le port appartient à ce qui sert l’application. `host` est toujours accepté mais ne lie
|
||||
rien ici ; **[Déployer et passer à l’échelle](deploy.md)** explique ce qu’il contrôle réellement.
|
||||
**[Exécuter votre serveur](index.md)** détaille les options elles-mêmes.
|
||||
|
||||
`mcp.sse_app()` fait la même chose pour le transport SSE, désormais remplacé.
|
||||
|
||||
## Localhost uniquement, jusqu’à ce que vous en décidiez autrement {#localhost-only-until-you-say-otherwise}
|
||||
|
||||
Par défaut, l’application répond **uniquement** aux requêtes adressées à localhost. `streamable_http_app()`
|
||||
ne peut pas savoir derrière quel nom d’hôte elle sera servie ; elle active donc la protection contre le DNS rebinding avec la
|
||||
liste d’autorisation la plus sûre possible ; sur votre machine, c’est exactement ce qu’il faut. Déployée derrière un vrai nom d’hôte,
|
||||
cela signifie que **chaque requête est rejetée avec `421 Misdirected Request`** tant que vous n’avez pas passé à
|
||||
`transport_security=` une liste d’autorisation de ce que vous servez réellement. Rien de ce que vous avez construit n’est même
|
||||
consulté avant. Cette liste d’autorisation, et tout ce qui sépare une application fonctionnelle d’un vrai nom d’hôte,
|
||||
c’est **[Déployer et passer à l’échelle](deploy.md)**.
|
||||
|
||||
## Le monter {#mounting-it}
|
||||
|
||||
Dès que le serveur MCP fait *partie* d’une application plus grande, vous placez l’application dans un `Mount`. Et dès que vous faites cela, le cycle de vie devient votre problème :
|
||||
|
||||
```python title="server.py" hl_lines="18-21 25-26"
|
||||
--8<-- "docs_src/asgi/tutorial002.py"
|
||||
```
|
||||
|
||||
* `Mount("/", ...)` combiné au chemin par défaut `/mcp` garde le point de terminaison à `/mcp`. Starlette essaie les routes dans l’ordre et `Mount("/")` correspond à **tous** les chemins ; vos propres routes vont donc *avant* lui dans la liste. Tout ce qui vient après est inaccessible.
|
||||
* La fonction `lifespan` entre dans `mcp.session_manager.run()` pour toute la durée de vie de l’application **hôte**. C’est la ligne que tout le monde oublie.
|
||||
* `mcp.session_manager` n’existe qu’*après* l’appel à `streamable_http_app()`. C’est pourquoi les routes sont construites au niveau du module et que le gestionnaire de sessions n’est manipulé qu’à l’intérieur du cycle de vie.
|
||||
|
||||
La route `Host` de Starlette fonctionne de la même façon : remplacez `Mount("/", ...)` par `Host("mcp.example.com", ...)` pour router par nom d’hôte plutôt que par chemin. La règle du cycle de vie ne change pas, et celle de la sécurité du transport non plus. Une route `Host("mcp.example.com", ...)` ne reçoit jamais que les requêtes adressées à ce nom d’hôte, mais la propre liste d’autorisation Host du transport (**[Déployer et passer à l’échelle](deploy.md)**) s’exécute tout de même en premier. Sans `"mcp.example.com"` dedans, cette route répond à chacune d’elles par un `421`.
|
||||
|
||||
!!! warning "L’application hôte possède le cycle de vie"
|
||||
`streamable_http_app()` branche `session_manager.run()` sur le cycle de vie de l’application Starlette qu’elle
|
||||
renvoie, mais **le cycle de vie d’une sous-application montée ne s’exécute jamais**. Montez l’application et ce
|
||||
cycle de vie intégré devient du code mort. L’application située au sommet de votre pile ASGI, quelle qu’elle soit, doit entrer dans
|
||||
`mcp.session_manager.run()` dans son propre cycle de vie.
|
||||
|
||||
!!! check
|
||||
Supprimez la ligne `lifespan=lifespan` et démarrez le serveur. Il démarre. La route se résout.
|
||||
Puis la première requête vers `/mcp` échoue avec :
|
||||
|
||||
```text
|
||||
RuntimeError: Task group is not initialized. Make sure to use run().
|
||||
```
|
||||
|
||||
Rien ne démarre le gestionnaire de sessions, si ce n’est sa méthode `run()`.
|
||||
|
||||
## Deux serveurs, une application {#two-servers-one-app}
|
||||
|
||||
Chaque `MCPServer` est sa propre application avec son propre gestionnaire de sessions. Montez-en autant que vous voulez ; entrez dans chaque gestionnaire depuis l’unique cycle de vie de l’hôte :
|
||||
|
||||
```python title="server.py" hl_lines="27-30 35-36"
|
||||
--8<-- "docs_src/asgi/tutorial003.py"
|
||||
```
|
||||
|
||||
* `AsyncExitStack` entre dans les deux gestionnaires ; ils démarrent ensemble et s’arrêtent dans l’ordre inverse.
|
||||
* Les points de terminaison sont `/notes/mcp` et `/tasks/mcp` : le préfixe de montage suivi du chemin par défaut.
|
||||
|
||||
## Changer le chemin {#changing-the-path}
|
||||
|
||||
Ce `/mcp` final, c’est `streamable_http_path`. Définissez-le à `"/"` et le préfixe de montage devient le chemin public complet :
|
||||
|
||||
```python title="server.py" hl_lines="25"
|
||||
--8<-- "docs_src/asgi/tutorial004.py"
|
||||
```
|
||||
|
||||
Les clients se connectent désormais à `/notes`, et non à `/notes/mcp`.
|
||||
|
||||
## CORS pour les clients navigateur {#cors-for-browser-clients}
|
||||
|
||||
Un client qui s’exécute dans un navigateur a besoin de deux permissions de votre part : **envoyer** ses en-têtes de requête MCP, et **lire** celui que MCP renvoie. Les deux relèvent de la configuration CORS de l’application hôte, et la liste d’autorisation de la sécurité du transport ci-dessus doit concorder avec elle :
|
||||
|
||||
```python title="server.py" hl_lines="27-30 33 35-49"
|
||||
--8<-- "docs_src/asgi/tutorial005.py"
|
||||
```
|
||||
|
||||
* `allow_headers` est la moitié que tout le monde oublie. Un navigateur envoie une **requête préliminaire** (preflight) avant chaque requête MCP, parce que `Content-Type: application/json` et les en-têtes de requête `Mcp-*` ne figurent pas dans la liste sûre de CORS, et un en-tête que la requête préliminaire n’accorde pas, c’est une requête que le navigateur n’envoie jamais. (`allow_headers=["*"]` fonctionne aussi : Starlette répond à une requête préliminaire avec ce qu’elle a demandé.)
|
||||
* `expose_headers=["Mcp-Session-Id"]` est la moitié lecture. Streamable HTTP renvoie l’identifiant de session dans cet en-tête de réponse, et les navigateurs masquent les en-têtes de réponse au JavaScript sauf si CORS les expose nommément. Sans lui, le client ne peut jamais faire sa deuxième requête.
|
||||
* `allow_origins` est votre décision, pas celle de MCP. Soyez précis, et reproduisez-le dans `allowed_origins=` ci-dessus : le navigateur applique CORS, mais le serveur vérifie lui-même l’en-tête `Origin`, et une origine à laquelle le transport ne fait pas confiance reçoit un `403` même après une requête préliminaire réussie.
|
||||
* `allow_methods` liste les trois méthodes qu’utilise Streamable HTTP : `POST` pour envoyer des messages, `GET` pour ouvrir le flux serveur vers client, `DELETE` pour terminer la session.
|
||||
|
||||
## Routes personnalisées {#custom-routes}
|
||||
|
||||
`@mcp.custom_route()` enregistre un point de terminaison HTTP ordinaire sur la même application, pour ce dont tout service déployé a besoin et qui n’a rien à voir avec MCP : une vérification d’état, un rappel OAuth.
|
||||
|
||||
```python title="server.py" hl_lines="15-17"
|
||||
--8<-- "docs_src/asgi/tutorial006.py"
|
||||
```
|
||||
|
||||
* Le gestionnaire est du Starlette ordinaire : une fonction `async` de `Request` vers `Response`.
|
||||
* `streamable_http_app()` récupère chaque route personnalisée. `app.routes` contient maintenant `/mcp` et `/health`.
|
||||
* `GET /health` répond `{"status": "ok"}` sans la moindre trace de MCP.
|
||||
|
||||
!!! warning
|
||||
Les routes personnalisées ne sont **jamais authentifiées**, même lorsque le reste du serveur l’est. C’est
|
||||
volontaire : les vérifications d’état et les rappels OAuth doivent être joignables avant qu’un quelconque jeton n’existe.
|
||||
Ne mettez rien de privé derrière l’une d’elles.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* `mcp.streamable_http_app()` renvoie une application Starlette avec une route, `/mcp`. N’importe quel serveur ASGI peut l’exécuter.
|
||||
* Par défaut, l’application répond uniquement aux requêtes adressées à localhost, et derrière un vrai nom d’hôte elle rejette tout avec un `421` tant que vous n’avez pas passé à `transport_security=` une liste d’autorisation. **[Déployer et passer à l’échelle](deploy.md)** s’occupe de cela, et du reste du chemin vers la production.
|
||||
* `Mount` (ou `Host`) la place dans une application Starlette ou FastAPI plus grande.
|
||||
* **Le montage désactive le cycle de vie intégré.** Le cycle de vie de l’application hôte doit entrer dans `mcp.session_manager.run()`, sinon la première requête échoue.
|
||||
* Plusieurs serveurs dans une même application, c’est plusieurs montages et un seul cycle de vie qui entre dans chaque gestionnaire de sessions.
|
||||
* `streamable_http_path="/"` déplace le point de terminaison sur le préfixe de montage lui-même.
|
||||
* Les clients navigateur ont besoin de CORS : `allow_headers` pour les en-têtes de requête `Mcp-*`, `expose_headers=["Mcp-Session-Id"]` pour la réponse.
|
||||
* `@mcp.custom_route()` ajoute des points de terminaison HTTP ordinaires, non authentifiés, à côté de `/mcp`.
|
||||
|
||||
Une fois le serveur joignable à une vraie URL, **[Le client](../client/index.md)** s’y connecte avec cette URL plutôt qu’avec un objet serveur.
|
||||
130
i18n/fr/pages/run/authorization.md
Normal file
130
i18n/fr/pages/run/authorization.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
---
|
||||
translation:
|
||||
sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31]
|
||||
tool: 1
|
||||
---
|
||||
# Autorisation {#authorization}
|
||||
|
||||
Sur Streamable HTTP, votre serveur MCP est un service web ordinaire, et vous le protégez comme n’importe quel service web : avec des jetons porteurs OAuth 2.1.
|
||||
|
||||
En termes OAuth, votre serveur est un **serveur de ressources**. Il ne connecte jamais personne et n’émet jamais de jeton. Il fait une seule chose : examiner l’en-tête `Authorization` de chaque requête et décider si le jeton qu’il contient est valable.
|
||||
|
||||
Cette page traite du côté serveur. Un client qui découvre votre serveur d’autorisation et récupère le jeton, c’est **[Clients OAuth](../client/oauth-clients.md)**.
|
||||
|
||||
## Les trois parties {#the-three-parties}
|
||||
|
||||
* Le **serveur d’autorisation** connecte les utilisateurs et émet les jetons d’accès. Vous ne l’écrivez pas. C’est votre fournisseur d’identité (Auth0, Keycloak, Entra, le vôtre).
|
||||
* Le **serveur de ressources**, c’est votre serveur MCP. Il vérifie le jeton à chaque requête.
|
||||
* Le **client** découvre à quel serveur d’autorisation vous faites confiance, en obtient un jeton et vous le renvoie sous la forme `Authorization: Bearer <token>`.
|
||||
|
||||
C’est tout le triangle. Toute cette page porte sur le point du milieu.
|
||||
|
||||
## Un vérificateur de jetons {#a-token-verifier}
|
||||
|
||||
Le SDK n’a aucun avis sur ce à quoi ressemble un jeton valide. C’est vous qui le lui dites, en implémentant **`TokenVerifier`** :
|
||||
|
||||
```python title="server.py" hl_lines="12-14 19-24"
|
||||
--8<-- "docs_src/authorization/tutorial001.py"
|
||||
```
|
||||
|
||||
* `TokenVerifier` est un protocole avec une seule méthode asynchrone. `verify_token` reçoit le jeton brut de l’en-tête `Authorization` et renvoie un **`AccessToken`** s’il est valide, `None` sinon. Il n’y a rien d’autre à implémenter.
|
||||
* Celui-ci cherche le jeton dans une table. Un vérificateur réel vérifie la signature d’un JWT ou appelle le point de terminaison d’introspection de jetons du serveur d’autorisation. Ce code est le vôtre ; le SDK ne fait que l’appeler.
|
||||
* `token_verifier=` et `auth=` vont toujours de pair. Passez l’un sans l’autre et `MCPServer(...)` lève une `ValueError` avant même de servir la moindre requête.
|
||||
|
||||
`AuthSettings` est la face publique de votre serveur de ressources :
|
||||
|
||||
* `issuer_url` : le serveur d’autorisation qui émet vos jetons.
|
||||
* `resource_server_url` : l’URL publique de ce point de terminaison MCP. Elle désigne *quelle* ressource un jeton vise, et c’est là que réside le document de découverte.
|
||||
* `required_scopes` : chaque jeton doit tous les porter.
|
||||
|
||||
!!! tip
|
||||
`examples/servers/simple-auth/` dans le dépôt du SDK contient un `IntrospectionTokenVerifier` qui appelle
|
||||
le point de terminaison [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) d’un véritable serveur d’autorisation. C’est la forme que prennent la plupart des vérificateurs en production.
|
||||
|
||||
## Ce que vous obtenez sur HTTP {#what-you-get-over-http}
|
||||
|
||||
L’autorisation vit dans les en-têtes HTTP, elle n’existe donc que sur les transports HTTP. Lancez-la sur celui que vous déployez : `mcp.run(transport="streamable-http")` la place sur `http://127.0.0.1:8000/mcp`, et le reste est dans **[Exécuter votre serveur](index.md)**. L’application possède désormais deux routes :
|
||||
|
||||
```text
|
||||
/mcp
|
||||
/.well-known/oauth-protected-resource/mcp
|
||||
```
|
||||
|
||||
Vous avez enregistré un seul outil. La seconde route est celle du SDK.
|
||||
|
||||
### Découverte {#discovery}
|
||||
|
||||
Faites un `GET` sur ce chemin well-known et vous obtenez les **métadonnées de ressource protégée de la [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)** (Protected Resource Metadata), construites directement à partir de vos `AuthSettings` :
|
||||
|
||||
```json
|
||||
{
|
||||
"resource": "http://127.0.0.1:8000/mcp",
|
||||
"authorization_servers": ["https://auth.example.com/"],
|
||||
"scopes_supported": ["notes:read"],
|
||||
"bearer_methods_supported": ["header"]
|
||||
}
|
||||
```
|
||||
|
||||
C’est grâce à ce document qu’un client qui n’a jamais entendu parler de votre serveur trouve son chemin : il lit `authorization_servers` et s’y rend pour obtenir un jeton. Vous n’en avez rien écrit.
|
||||
|
||||
!!! check
|
||||
Appelez `/mcp` sans jeton (ou avec un jeton pour lequel votre vérificateur a renvoyé `None`) et la requête
|
||||
est arrêtée à la porte :
|
||||
|
||||
```text
|
||||
HTTP/1.1 401 Unauthorized
|
||||
WWW-Authenticate: Bearer error="invalid_token", error_description="Authentication required", resource_metadata="http://127.0.0.1:8000/.well-known/oauth-protected-resource/mcp"
|
||||
|
||||
{"error": "invalid_token", "error_description": "Authentication required"}
|
||||
```
|
||||
|
||||
Rien n’a été analysé et aucun outil n’a été exécuté. Et ce pointeur `resource_metadata` dans `WWW-Authenticate` est
|
||||
ce qui rend la découverte automatique : 401 -> document de métadonnées -> serveur d’autorisation -> jeton -> nouvelle tentative.
|
||||
|
||||
!!! warning
|
||||
Rien de tout cela ne protège `stdio`. Un tube n’a pas d’en-tête `Authorization`, donc `token_verifier` n’y est
|
||||
jamais consulté. La frontière de sécurité d’un serveur `stdio` est le processus qui l’a lancé. Il en va de
|
||||
même pour le `Client(mcp)` en mémoire que vous utilisez dans les tests : il se connecte directement à l’objet serveur
|
||||
et saute la couche HTTP, autorisation comprise.
|
||||
|
||||
## L’identité de l’appelant {#the-callers-identity}
|
||||
|
||||
Dans n’importe quel gestionnaire (handler), **`get_access_token()`** est l’objet `AccessToken` que votre vérificateur a renvoyé pour la requête en cours :
|
||||
|
||||
```python title="server.py" hl_lines="4 32-35"
|
||||
--8<-- "docs_src/authorization/tutorial002.py"
|
||||
```
|
||||
|
||||
* Cela fonctionne dans les outils, les ressources et les prompts, et il n’y a rien à transmettre : le middleware d’authentification le stocke dans une variable de contexte par requête.
|
||||
* Vous récupérez le **même objet que celui construit par votre vérificateur** : `client_id`, `scopes`, `subject`, `expires_at` et tous les `claims` supplémentaires que vous y avez attachés. C’est le point d’accroche pour des règles par outil : lisez les scopes et refusez.
|
||||
* En dehors d’une requête HTTP authentifiée, elle renvoie `None`. En mémoire et sur `stdio`, c’est toujours `None`.
|
||||
|
||||
Appelez `whoami` avec `Authorization: Bearer alice-token` et le modèle lit :
|
||||
|
||||
```text
|
||||
alice (scopes: notes:read)
|
||||
```
|
||||
|
||||
## La moitié que le SDK ne fait pas {#the-half-the-sdk-doesnt-do}
|
||||
|
||||
Le SDK vous donne la moitié serveur de ressources : vérifier, annoncer, refuser. Il ne vous donne ni page de connexion, ni écran de consentement, ni jeton.
|
||||
|
||||
Pour voir les trois parties en action, lancez `examples/servers/simple-auth/` depuis le dépôt du SDK (un petit serveur d’autorisation et un serveur de ressources configuré exactement comme sur cette page), puis pointez `examples/clients/simple-auth-client/` dessus pour la chorégraphie complète découverte-puis-jeton.
|
||||
|
||||
!!! info
|
||||
Il existe un second argument de constructeur, `auth_server_provider=`, qui embarque un serveur d’autorisation
|
||||
complet dans votre serveur MCP. Il est antérieur à la séparation AS/RS autour de laquelle la spécification
|
||||
d’autorisation MCP est construite. Les nouveaux serveurs ne devraient pas y recourir.
|
||||
|
||||
Un serveur d’autorisation peut aussi accepter l’assertion signée d’un fournisseur d’identité d’entreprise à la place d’un utilisateur qui valide un écran de consentement, et le SDK prend en charge les deux côtés de cet échange. Ce mode d’octroi (grant), et le client qui le présente, c’est **[Assertion d’identité](../client/identity-assertion.md)**.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* Sur Streamable HTTP, votre serveur est un **serveur de ressources** OAuth 2.1 : il vérifie les jetons, il n’en émet jamais.
|
||||
* `TokenVerifier` est toute la surface d’intégration : une méthode asynchrone, un jeton en entrée, `AccessToken | None` en sortie.
|
||||
* `token_verifier=` et `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` vont toujours de pair.
|
||||
* Le SDK publie les métadonnées de ressource protégée (Protected Resource Metadata) de la [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) sur `/.well-known/oauth-protected-resource/...` et répond aux requêtes non authentifiées par un 401 dont l’en-tête `WWW-Authenticate` pointe vers elles. C’est tout le mécanisme de découverte.
|
||||
* `get_access_token()` dans n’importe quel gestionnaire indique qui appelle.
|
||||
* L’autorisation est une affaire de HTTP. `stdio` et le client en mémoire ne la voient jamais.
|
||||
|
||||
La moitié client (découvrir votre serveur d’autorisation et récupérer le jeton pour vous), c’est **[Clients OAuth](../client/oauth-clients.md)**. Et un client qui *affirme* une identité au lieu d’en demander une à un utilisateur, c’est **[Assertion d’identité](../client/identity-assertion.md)**.
|
||||
182
i18n/fr/pages/run/deploy.md
Normal file
182
i18n/fr/pages/run/deploy.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
---
|
||||
translation:
|
||||
sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a]
|
||||
tool: 1
|
||||
---
|
||||
# Déployer et passer à l’échelle {#deploy-scale}
|
||||
|
||||
Votre serveur fonctionne. Il lui faut maintenant un vrai nom d’hôte, et plus d’un worker derrière lui.
|
||||
|
||||
Presque rien de tout cela ne regarde MCP. Vous apportez le serveur ASGI, le gestionnaire de processus, le répartiteur de charge. Ce que contient cette page, c’est la courte liste de ce qui *regarde* bel et bien MCP : un réglage qui conditionne tout déploiement, et les deux endroits où « plus d’un worker » change ce que fait le SDK.
|
||||
|
||||
## Avant toute chose : la liste des hôtes autorisés {#before-anything-else-the-host-allowlist}
|
||||
|
||||
`streamable_http_app()` ne peut pas savoir derrière quel nom d’hôte il sera servi, il retient donc la réponse la plus sûre : localhost. Sans `transport_security=`, l’application active la **protection contre le DNS rebinding** et n’accepte une requête que si son en-tête `Host` vaut `127.0.0.1:<port>`, `localhost:<port>` ou `[::1]:<port>`. L’en-tête `Origin`, quand il y en a un, doit être la forme `http://` du même hôte. Sur votre machine, c’est exactement ce qu’il faut : cela empêche une page web malveillante de piloter votre serveur local via un nom DNS qu’elle a fait pointer vers `127.0.0.1`.
|
||||
|
||||
Déployée derrière un vrai nom d’hôte, cette même valeur par défaut rejette **toutes les requêtes** tant que vous ne dites pas le contraire. La vérification s’exécute avant tout ce qui ressemble à du MCP, si bien que rien de ce que vous avez construit n’est même consulté :
|
||||
|
||||
```text
|
||||
421 Misdirected Request Invalid Host header the Host is not in the allowlist
|
||||
403 Forbidden Invalid Origin header the Origin is not in the allowlist
|
||||
```
|
||||
|
||||
`transport_security=` est le correctif. Autorisez ce que vous servez réellement :
|
||||
|
||||
```python title="server.py" hl_lines="2 13-17"
|
||||
--8<-- "docs_src/deploy/tutorial001.py"
|
||||
```
|
||||
|
||||
* Les entrées de `allowed_hosts` sont des chaînes exactes : `"mcp.example.com"` correspond à un en-tête `Host` sans port et `"mcp.example.com:*"` correspond à n’importe quel port. Listez les deux.
|
||||
* `allowed_origins` ne compte que pour les navigateurs, car rien d’autre n’envoie `Origin`. C’est le pendant côté serveur de la configuration CORS décrite dans **[Ajouter à une application existante](asgi.md)**.
|
||||
* Derrière un proxy inverse qui contrôle déjà l’en-tête `Host`, désactiver la vérification est la configuration honnête : `TransportSecuritySettings(enable_dns_rebinding_protection=False)`.
|
||||
* Passer un `host=` autre que localhost (par exemple `host="mcp.example.com"`) n’autorise **pas** ce nom d’hôte. Cela empêche seulement la valeur par défaut localhost d’armer la protection, ce qui laisse passer tous les Host et tous les Origin. Dites plutôt ce que vous voulez avec `transport_security=`.
|
||||
|
||||
!!! check
|
||||
Supprimez l’argument `transport_security=security` et déployez quand même l’application. Elle
|
||||
démarre, `/mcp` route, et chaque requête (y compris depuis un simple `curl`) revient avec :
|
||||
|
||||
```text
|
||||
HTTP/1.1 421 Misdirected Request
|
||||
|
||||
Invalid Host header
|
||||
```
|
||||
|
||||
Vous ne trouverez pas ces mots côté client. Un `421` est une réponse HTTP en texte brut, pas une
|
||||
erreur JSON-RPC, si bien que le client MCP lève une erreur de transport générique ; le nom d’hôte
|
||||
qu’il n’a pas apprécié n’apparaît que dans le journal du **serveur**, sous la forme d’un unique
|
||||
avertissement. Un serveur fraîchement déployé qui refuse toutes les connexions est un problème
|
||||
de liste des hôtes autorisés jusqu’à preuve du contraire.
|
||||
**[Dépannage](../troubleshooting.md)** commence aussi par là.
|
||||
|
||||
## Les workers, et qui a besoin d’affinité {#workers-and-who-has-to-be-sticky}
|
||||
|
||||
Une fois que le nom d’hôte répond, placez plus d’un worker derrière lui. Le SDK n’a aucun réglage pour cela ; vous passez une application Starlette à l’échelle comme n’importe quelle application ASGI, en confiant l’objet à quelque chose qui sait créer des processus (fork) :
|
||||
|
||||
```console
|
||||
uvicorn server:app --workers 4
|
||||
```
|
||||
|
||||
Quatre processus, un socket. Et maintenant la question à laquelle tout déploiement doit répondre : **une requête doit-elle atteindre le worker qui a vu la précédente ?**
|
||||
|
||||
Pour un client qui parle le protocole **2026-07-28**, non. Une requête moderne est un unique POST autonome : pas de poignée de main (handshake) `initialize` avant elle, pas de `Mcp-Session-Id` sur la réponse, rien *vers quoi* une deuxième requête devrait revenir. Routez-la vers n’importe quel worker.
|
||||
|
||||
Ce n’est pas un mode que vous activez. `stateless_http=True` en a tout l’air, mais le transport route d’après l’en-tête de requête `MCP-Protocol-Version`, confie une requête moderne au gestionnaire moderne, et **rend la main**. La ligne qui lit `stateless_http` vient *après* ce retour. Ce n’est pas que l’indicateur soit ignoré sur le chemin 2026-07-28 ; il n’est jamais atteint. `stateless_http` est un réglage pour la branche **historique** uniquement, et le chemin moderne est sans session par construction.
|
||||
|
||||
Pour un client historique en version 2025-11-25 de la spécification ou antérieure, la réponse dépend de cet indicateur :
|
||||
|
||||
| Version du protocole du client | Session | Ce que le répartiteur de charge doit faire |
|
||||
| --- | --- | --- |
|
||||
| **2026-07-28** | Aucune. `Mcp-Session-Id` n’est jamais défini. | Rien. N’importe quel worker sert n’importe quelle requête. |
|
||||
| **2025-11-25 et antérieures** (par défaut) | `Mcp-Session-Id`, conservé dans la mémoire d’un seul worker. | **Affinité de session (sticky sessions).** Une requête suivante qui atteint un autre worker reçoit un `404` *« Session not found »*. |
|
||||
| **2025-11-25 et antérieures**, avec `stateless_http=True` | Aucune. | Rien. Le prix à payer est le canal de retour (back-channel) du serveur vers le client — échantillonnage (sampling), élicitation (elicitation) en push, `roots/list` — et la reprise. |
|
||||
|
||||
L’affinité de session et le coût de la branche historique ont leur propre page, **[Prendre en charge les clients historiques](legacy-clients.md)** ; les deux générations elles-mêmes sont décrites dans **[Versions du protocole](../protocol-versions.md)**. Ce qui compte ici, c’est la forme de la réponse : *en version 2026-07-28, vous êtes déjà sans état, sans rien à configurer.*
|
||||
|
||||
Le reste de cette page porte sur les deux choses que l’absence d’état ne vous apporte **pas**.
|
||||
|
||||
## `requestState` d’un worker à l’autre {#requeststate-across-workers}
|
||||
|
||||
Un outil **[à plusieurs allers-retours (multi-round-trip)](../handlers/multi-round-trip.md)** a besoin de quelque chose que le client doit aller chercher (une confirmation, un choix, un identifiant), il renvoie donc une question au lieu d’une réponse et termine lors de la nouvelle tentative. Entre les deux tours, le client détient un jeton `request_state` opaque émis par le serveur. Lors de la nouvelle tentative, le serveur doit rouvrir ce jeton.
|
||||
|
||||
*Scellé sous quelle clé ?* Par défaut, une clé que le serveur a générée avec `os.urandom(32)` au moment de sa construction. Avec `--workers 4`, cela fait quatre constructions, dans quatre processus : quatre clés différentes, jamais écrites nulle part, jamais partagées, perdues au redémarrage.
|
||||
|
||||
Voici un outil qui demande avant d’agir, sur un serveur qui ne configure rien :
|
||||
|
||||
```python title="server.py" hl_lines="14 20"
|
||||
--8<-- "docs_src/deploy/tutorial002.py"
|
||||
```
|
||||
|
||||
Le premier tour atteint le worker A. Le worker A scelle `refund:120` sous **sa** clé et renvoie le jeton. Le client présente la question à une personne, obtient un oui, et retente. La nouvelle tentative est une requête HTTP toute neuve.
|
||||
|
||||
!!! check
|
||||
Laissez cette nouvelle tentative atteindre le worker B. B essaie de desceller un jeton qu’il n’a
|
||||
pas émis, n’y parvient pas, et refuse tout le tour. `refund` n’est jamais appelé ; le client
|
||||
reçoit une erreur JSON-RPC :
|
||||
|
||||
```json
|
||||
{
|
||||
"code": -32602,
|
||||
"message": "Invalid or expired requestState",
|
||||
"data": {"reason": "invalid_request_state"}
|
||||
}
|
||||
```
|
||||
|
||||
Ce message est **figé**. Expiré, falsifié, rejoué avec des arguments différents, ou (de loin la
|
||||
cause la plus fréquente dans un vrai déploiement) scellé par un worker voisin : le client reçoit
|
||||
chaque fois la même chose, si bien que la liaison ne révèle jamais quelle vérification a échoué.
|
||||
La vraie raison est un unique `WARNING` dans le journal du serveur :
|
||||
|
||||
```text
|
||||
requestState rejected on tools/call: unknown key
|
||||
```
|
||||
|
||||
Un outil à plusieurs allers-retours qui fonctionnait avec un worker et s’est mis à échouer *de
|
||||
temps en temps* avec deux, c’est cela. Les deux tours doivent toujours atteindre le même
|
||||
processus, il échoue donc exactement aussi souvent que votre répartiteur de charge les sépare.
|
||||
|
||||
Les deux tours sont deux requêtes HTTP indépendantes, et plusieurs choses ordinaires les séparent : un proxy qui répartit requête par requête, une connexion tombée entre les deux, un déploiement ou un redémarrage, un client qui a persisté `request_state` et reprend depuis un tout autre processus (**[Piloter la boucle vous-même](../handlers/multi-round-trip.md#driving-the-loop-yourself)**). Chacune d’elles revient à « un autre worker ».
|
||||
|
||||
Le correctif tient en un argument. Il a **deux** moitiés.
|
||||
|
||||
```python title="server.py" hl_lines="1 12 14"
|
||||
--8<-- "docs_src/deploy/tutorial003.py"
|
||||
```
|
||||
|
||||
* **`keys=[...]`** est la moitié que tout le monde trouve. Donnez à chaque instance le même secret (au moins 32 octets), et chaque instance peut desceller ce que n’importe quelle autre a émis. `keys[0]` scelle et chaque clé de la liste descelle, ce qui forme l’anneau de rotation ; **[Faire tourner les clés](../handlers/multi-round-trip.md#rotating-keys)** explique comment le faire tourner sans interruption de service.
|
||||
* **Le nom du serveur** est la moitié que presque personne ne trouve, et la raison pour laquelle les nouvelles tentatives entre instances échouent encore après avoir partagé la clé. Chaque jeton scellé porte le `name` du serveur comme **revendication d’audience** (audience claim), vérifiée strictement au retour. Deux instances construites à partir du même code ont le même nom et ne le remarquent jamais. Nommez-les différemment (`MCPServer(f"billing-{POD}")` ressemble à une bonne hygiène d’observabilité), et chaque nouvelle tentative entre instances est refusée exactement comme ci-dessus, clé partagée ou non. Le journal indique `audience` au lieu de `unknown key` ; le client ne voit pas la différence.
|
||||
|
||||
Générez le secret une fois et donnez la même valeur à chaque instance. C’est la commande que le message d’erreur du SDK lui-même vous indique d’exécuter si vous lui passez moins de 32 octets :
|
||||
|
||||
```console
|
||||
python -c "import secrets; print(secrets.token_hex(32))"
|
||||
```
|
||||
|
||||
!!! warning "Les mêmes clés, *et* le même nom"
|
||||
Un déploiement à plusieurs instances doit partager les deux. Si les noms par instance comptent
|
||||
vraiment pour vous, donnez plutôt une audience explicite à toute la flotte :
|
||||
`RequestStateSecurity(keys=[...], audience="billing")`. Chaque instance émet et accepte alors
|
||||
sous `"billing"`, quel que soit son nom.
|
||||
|
||||
Tout le reste sur le scellement se trouve dans **[Protéger `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)** : ce qu’il lie, le `ttl` par tour (600 secondes par défaut), apporter votre propre codec, pourquoi la valeur par défaut non configurée est exactement ce qu’il faut sur `stdio`. Toute la contribution de cette page tient en une liste de contrôle à deux éléments : *mêmes clés, même nom.*
|
||||
|
||||
!!! info
|
||||
Vous êtes sur ce chemin même si vous n’avez jamais tapé `InputRequiredResult`. Un outil dont les
|
||||
paramètres utilisent `Resolve(...)` (**[Dépendances](../handlers/dependencies.md)**) est un outil
|
||||
à plusieurs allers-retours, et le SDK émet et scelle son `request_state` pour lui. Même clé par
|
||||
défaut, même échec entre workers, même correctif.
|
||||
|
||||
## Notifications de changement d’une réplique à l’autre {#change-notifications-across-replicas}
|
||||
|
||||
Le flux `subscriptions/listen` d’un client est une unique réponse de longue durée, il est donc épinglé à une réplique pendant toute sa durée de vie. Un `ctx.notify_resource_updated(...)` publié sur une **autre** réplique doit l’atteindre.
|
||||
|
||||
La jonction entre les deux est le `SubscriptionBus`. Le bus que vous donnez à un serveur est celui où va chaque publication et sur lequel écoute chaque flux ouvert ; donnez donc le même bus à chaque réplique :
|
||||
|
||||
```python title="server.py" hl_lines="2 7 9"
|
||||
--8<-- "docs_src/deploy/tutorial004.py"
|
||||
```
|
||||
|
||||
Rien dans la diffusion ne se soucie de l’objet serveur auquel un flux est attaché. Deux serveurs qui partagent un même `InMemorySubscriptionBus` se comportent déjà ainsi : ouvrez un flux d’écoute sur l’un, appelez `edit_note` sur l’autre, et le flux en est informé. Ce bus en mémoire ne couvre que les objets serveur d’un même processus, ce qui en fait le modèle, pas le déploiement :
|
||||
|
||||
* Entre de vrais processus, **le SDK ne fournit aucun bus qui puisse vous aider.** `SubscriptionBus` est un `Protocol` à deux méthodes (`publish` et `subscribe`) que vous implémentez par-dessus votre propre backend pub/sub (Redis, NATS, ce que vous exploitez déjà) et passez sous la forme `MCPServer(subscriptions=...)`. **[Abonnements](../handlers/subscriptions.md#scaling-past-one-process)** contient l’esquisse et le contrat.
|
||||
* Le bus transporte quatre petits événements typés, jamais de JSON-RPC. L’accusé de réception, le filtrage et le cycle de vie des flux restent dans le SDK, si bien que votre bus ne peut pas casser le protocole ; il ne peut que déplacer des événements entre processus.
|
||||
* Les flux ne sont **pas** reprenables et les événements ne sont **pas** rejoués. Perdre une réplique abandonne ses flux ; les clients se remettent à l’écoute et récupèrent de nouveau les données. Il n’y a pas de magasin d’événements à partager et rien d’autre à configurer. C’est le seul endroit où la montée en charge horizontale revient réellement à faire la même chose en plus grand.
|
||||
|
||||
## Ce que le SDK ne vous donne pas {#what-the-sdk-does-not-give-you}
|
||||
|
||||
Un `MCPServer` est une implémentation du protocole, pas un serveur d’applications. Les réglages de déploiement que vous chercherez ensuite manquent volontairement :
|
||||
|
||||
* **Pas de `workers=`.** `mcp.run("streamable-http")` démarre exactement un processus uvicorn, et c’est tout ce qu’il démarrera jamais. Le multi-processus, c’est `streamable_http_app()` confié à ce avec quoi vous déployez déjà de l’ASGI : `uvicorn --workers`, gunicorn, le gestionnaire de processus de votre plateforme. Cette page n’est délibérément un tutoriel pour aucun d’eux ; leur documentation est meilleure que ne le serait une copie ici.
|
||||
* **Pas de route de contrôle de santé.** `@mcp.custom_route("/health", methods=["GET"])` est toute la réponse, et elle n’est jamais authentifiée même quand le reste du serveur l’est. C’est ce qu’il faut pour une sonde de vivacité, pas pour quoi que ce soit de privé. **[Ajouter à une application existante](asgi.md#custom-routes)** en montre une.
|
||||
* **Pas d’objet de réglages de production.** Il n’y a nulle part sur `MCPServer` où noter les délais d’expiration, TLS, l’arrêt progressif ou les limites de connexions, parce que rien de cela n’est son travail. Cela relève de votre serveur ASGI, et c’est là que vous le configurez. **[Exécuter votre serveur](index.md)** couvre la poignée de réglages que le constructeur accepte *effectivement*.
|
||||
* **Pas de `EventStore` fourni, et en version 2026-07-28 aucun usage pour un tel objet.** La reprise est une fonctionnalité de la branche historique avec état ; un échange moderne, c’est un POST, une réponse, et rien à reprendre.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* Par défaut, l’application ne répond qu’aux requêtes adressées à localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` est le passage obligé avant la mise en production : tant que vous ne le passez pas, chaque requête derrière un vrai nom d’hôte est un `421` et la raison n’est que dans le journal du serveur.
|
||||
* En version 2026-07-28, il n’y a pas de session et rien sur quoi un répartiteur de charge pourrait établir une affinité. `stateless_http=True` est un réglage réservé à la branche historique, parce qu’une requête moderne est routée et traitée avant même que cet indicateur soit lu.
|
||||
* La clé `requestState` par défaut est `os.urandom(32)`, générée par processus. Une nouvelle tentative à plusieurs allers-retours qui atteint un autre worker échoue avec `-32602` *« Invalid or expired requestState »*.
|
||||
* Le correctif est `RequestStateSecurity(keys=[...])` **et** le même nom de serveur sur chaque instance. Le nom est la revendication d’audience par défaut du jeton. Mêmes clés, même nom.
|
||||
* Les notifications de changement traversent les répliques via un unique `SubscriptionBus` partagé. La seule implémentation du SDK fonctionne dans un seul processus ; le `Protocol` à deux méthodes par-dessus votre propre pub/sub, c’est à vous de l’écrire.
|
||||
* Il n’y a pas de `workers=`, pas de route de santé, pas d’objet de réglages de production. Apportez votre propre serveur ASGI.
|
||||
|
||||
L’autre chose dont un vrai nom d’hôte a besoin devant lui, c’est un jeton : **[Autorisation](authorization.md)**.
|
||||
156
i18n/fr/pages/run/index.md
Normal file
156
i18n/fr/pages/run/index.md
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
---
|
||||
translation:
|
||||
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 7a53ead3e704a7f0, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
|
||||
tool: 1
|
||||
---
|
||||
# Exécuter votre serveur {#running-your-server}
|
||||
|
||||
`mcp.run()` démarre le serveur.
|
||||
|
||||
La seule décision que vous prenez concerne le **transport** : la façon dont les octets circulent réellement entre votre serveur et son client.
|
||||
|
||||
## Choisir un transport {#pick-a-transport}
|
||||
|
||||
| Transport | Ce que c’est | Quand |
|
||||
|---|---|---|
|
||||
| `stdio` | L’hôte lance votre fichier comme sous-processus et communique via son stdin et son stdout. | Serveurs locaux. La valeur par défaut. |
|
||||
| `streamable-http` | Un véritable serveur HTTP qui écoute sur un port. | Tout ce que vous déployez. |
|
||||
| `sse` | L’ancien transport HTTP. | Jamais. |
|
||||
|
||||
!!! warning
|
||||
SSE a été remplacé par Streamable HTTP dans la révision 2025-03-26 du protocole.
|
||||
`mcp.run(transport="sse")` fonctionne toujours, avec ses propres options `sse_path=` et `message_path=`,
|
||||
mais il n’existe que pour les clients qui n’ont pas encore migré. Ne construisez rien de nouveau dessus.
|
||||
|
||||
## `mcp.run()` {#mcprun}
|
||||
|
||||
```python title="server.py" hl_lines="12-13"
|
||||
--8<-- "docs_src/run/tutorial001.py"
|
||||
```
|
||||
|
||||
* `run()` est synchrone. Elle bloque pendant toute la durée de vie du serveur.
|
||||
* Sans argument, le transport est `stdio`.
|
||||
* Elle se trouve sous `if __name__ == "__main__":` parce que tout ce qui charge votre serveur (`mcp dev`, `mcp run`, `mcp install`, vos tests) **importe** ce fichier. La garde empêche un import de se transformer en serveur en cours d’exécution.
|
||||
|
||||
### stdio {#stdio}
|
||||
|
||||
Il n’y a rien à configurer. L’hôte démarre votre fichier comme processus enfant, écrit les requêtes sur son stdin et lit les réponses sur son stdout.
|
||||
|
||||
Lancez-le vous-même et vous en voyez la conséquence :
|
||||
|
||||
```console
|
||||
python server.py
|
||||
```
|
||||
|
||||
Rien ne s’affiche, et le programme ne rend pas la main. Il attend sur stdin qu’un hôte parle en premier.
|
||||
|
||||
Cela signifie aussi que stdout **est la liaison elle-même**. Pendant le service, le SDK déplace la liaison vers un descripteur privé et redirige vers stderr la sortie *vidée* sur stdout (un sous-processus qui écrit sur son stdout hérité, un `print()` vidé), où elle ne peut pas corrompre le flux. La sortie vidée sur stdout *avant* le début du service (un script d’enrobage qui affiche quelque chose, un print non tamponné au moment de l’import) atterrit toujours sur la liaison, de même qu’un `print()` qui reste en tampon jusqu’à ce que l’interpréteur le vide à la sortie. Pour la sortie que vous voulez réellement, le module `logging` est le bon outil : son gestionnaire vide chaque enregistrement sur stderr au moment où il se produit. Tous les détails sont dans **[Journalisation](../handlers/logging.md)**.
|
||||
|
||||
### Essayer {#try-it}
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
L’Inspector fait exactement ce que fait un véritable hôte : il lance `server.py` comme sous-processus et s’y connecte via stdio.
|
||||
|
||||
Vous ne lui avez jamais donné de port. Il n’y en a pas.
|
||||
|
||||
## Streamable HTTP {#streamable-http}
|
||||
|
||||
Pour placer le même serveur sur un port à la place, nommez le transport (et ses options) dans `run()` :
|
||||
|
||||
```python title="server.py" hl_lines="13"
|
||||
--8<-- "docs_src/run/tutorial002.py"
|
||||
```
|
||||
|
||||
Cette seule ligne construit une application Starlette et la sert avec uvicorn. Les clients se connectent à `http://127.0.0.1:3001/mcp`.
|
||||
|
||||
Chaque transport a ses propres arguments nommés, tous sur `run()` :
|
||||
|
||||
* `host` / `port` : où écouter. Valeurs par défaut `127.0.0.1` et `8000`.
|
||||
* `streamable_http_path` : où se trouve le point de terminaison MCP. Valeur par défaut `/mcp`.
|
||||
* `json_response=True` : répondre à chaque POST par un corps JSON unique au lieu d’un flux SSE. Ce corps a de la place pour la réponse et rien d’autre : un outil qui rappelle le client en cours de requête (`ctx.elicit()`, échantillonnage) lève donc `NoBackChannelError` sur ce tronçon, et les notifications liées à l’appel en cours (la progression de `ctx.report_progress()`, les messages de journal par appel) sont abandonnées ; le flux `GET` autonome transporte toujours celles qui n’y sont pas liées.
|
||||
* `stateless_http=True` : un transport neuf par requête, sans suivi de session.
|
||||
* `max_request_body_size` : la taille maximale acceptée pour le corps d’un POST, en octets. Vaut 4 Mio par défaut ; les requêtes plus grandes
|
||||
reçoivent un HTTP 413 avant toute analyse ou création de session. Ne l’augmentez que lorsque des messages MCP légitimes
|
||||
dépassent cette taille.
|
||||
* `event_store`, `retry_interval`, `transport_security` : reprise après coupure et protection contre le DNS rebinding. Ils peuvent attendre, jusqu’à ce que vous déployiez ailleurs que sur localhost ; **[Déployer et passer à l’échelle](deploy.md)** couvre `transport_security`.
|
||||
|
||||
!!! warning
|
||||
Les options de transport vont à `run()`, **pas** à `MCPServer(...)`. Le constructeur décrit ce que
|
||||
votre serveur *est* : nom, version, instructions. `run()` décrit comment il est servi. Inversez-les
|
||||
et Python répond avant même que MCP n’entre en jeu :
|
||||
|
||||
```text
|
||||
TypeError: MCPServer.__init__() got an unexpected keyword argument 'port'
|
||||
```
|
||||
|
||||
`run()` est le chemin court. Dès que vous avez besoin de plus (votre serveur monté dans une application existante, deux serveurs dans un même processus, CORS pour les clients navigateur), vous construisez l’application ASGI vous-même et la confiez à n’importe quel hôte ASGI. C’est **[Ajouter à une application existante](asgi.md)**.
|
||||
|
||||
## Paramètres du serveur {#server-settings}
|
||||
|
||||
Quelques aspects de l’exécution ne concernent pas le transport. Ce sont des arguments du constructeur :
|
||||
|
||||
```python title="server.py" hl_lines="3"
|
||||
--8<-- "docs_src/run/tutorial003.py"
|
||||
```
|
||||
|
||||
* `log_level` : transmis à `logging.basicConfig()` au moment où `MCPServer(...)` est construit. Cela configure le logger **racine**, et fixe donc le niveau de vos propres loggers aussi, pas seulement ceux du SDK. Valeur par défaut `"INFO"`.
|
||||
* `debug` : transmis à l’application Starlette que construisent les transports HTTP. Valeur par défaut `False`.
|
||||
|
||||
Les deux atterrissent sur `mcp.settings`, que vous pouvez relire à l’exécution.
|
||||
|
||||
## La commande `mcp` {#the-mcp-command}
|
||||
|
||||
L’extra `[cli]` installe un petit outil en ligne de commande autour de tout cela.
|
||||
|
||||
`mcp dev` exécute votre serveur sous le **MCP Inspector** :
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
uv run mcp dev server.py --with pandas --with numpy
|
||||
uv run mcp dev server.py --with-editable .
|
||||
```
|
||||
|
||||
`--with` ajoute des paquets à l’environnement qu’il construit ; `--with-editable` y installe votre propre paquet. Il a besoin de `npx` dans votre `PATH` : l’Inspector est une application Node.js.
|
||||
|
||||
`mcp run` importe le fichier, trouve l’objet serveur (un `mcp`, `server` ou `app` au niveau du module) et appelle `run()` dessus :
|
||||
|
||||
```console
|
||||
uv run mcp run server.py
|
||||
uv run mcp run server.py:bookshop
|
||||
```
|
||||
|
||||
Le suffixe `:` nomme l’objet lorsqu’il ne s’appelle pas `mcp`, `server` ou `app`.
|
||||
|
||||
Votre bloc `if __name__ == "__main__":` ne s’exécute jamais ici : `mcp run` appelle `run()` lui-même, et la seule option qu’il transmet est `--transport`.
|
||||
|
||||
`mcp install` enregistre le serveur auprès de **Claude Desktop**, pour que l’application le lance pour vous :
|
||||
|
||||
```console
|
||||
uv run mcp install server.py --name "Bookshop"
|
||||
uv run mcp install server.py -v API_KEY=abc123 -f .env
|
||||
```
|
||||
|
||||
`-v KEY=VALUE` et `-f .env` consignent des variables d’environnement dans cette entrée. Claude Desktop démarre votre serveur dans son propre processus. L’environnement de votre shell n’y est pas.
|
||||
|
||||
Claude Desktop est le seul hôte que `mcp install` connaît. Tous les autres hôtes (Claude Code, Cursor, VS Code) prennent la même commande de lancement dans leur propre fichier de configuration, et **[Se connecter à un véritable hôte](../get-started/real-host.md)** détaille chacun d’eux.
|
||||
|
||||
`mcp version` affiche la version du SDK installée.
|
||||
|
||||
!!! tip
|
||||
`mcp dev` et `mcp run` ne comprennent que `MCPServer`. Si vous construisez avec le `Server` bas niveau,
|
||||
vous l’exécutez vous-même. Voir **[Le Server bas niveau](../advanced/low-level-server.md)**.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* Un **transport** est la façon dont les octets atteignent votre serveur : `stdio` pour un sous-processus local, `streamable-http` pour un port. SSE est remplacé.
|
||||
* `mcp.run()` choisit le transport. Sans argument, c’est `stdio`, et elle bloque.
|
||||
* Chaque option de transport (`host`, `port`, `streamable_http_path`, ...) est un argument de `run()`, jamais de `MCPServer(...)`.
|
||||
* Gardez `run()` sous `if __name__ == "__main__":`. Tout ce qui charge votre serveur importe d’abord le fichier.
|
||||
* `log_level=` et `debug=` sont des arguments du constructeur ; ils atterrissent sur `mcp.settings`.
|
||||
* `mcp dev` pour l’Inspector, `mcp run` pour exécuter un fichier, `mcp install` pour Claude Desktop, `mcp version` pour la version.
|
||||
* Le transport ne change jamais ce que votre serveur *est* : les trois fichiers de cette page exposent le même outil, à l’identique.
|
||||
|
||||
Quand `run()` elle-même est la limite (votre serveur à l’intérieur d’une application qui existe déjà), c’est **[Ajouter à une application existante](asgi.md)**. Un vrai nom d’hôte et plus d’un worker, c’est **[Déployer et passer à l’échelle](deploy.md)**. Et si certains de vos clients sont encore sur la version 2025-11-25 de la spécification ou une version antérieure, **[Prendre en charge les clients historiques](legacy-clients.md)** est la bonne nouvelle.
|
||||
135
i18n/fr/pages/run/legacy-clients.md
Normal file
135
i18n/fr/pages/run/legacy-clients.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
---
|
||||
translation:
|
||||
sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c]
|
||||
tool: 1
|
||||
---
|
||||
# Prendre en charge les clients historiques {#serving-legacy-clients}
|
||||
|
||||
MCP a deux générations de protocole : la génération de la poignée de main (handshake) `initialize`, jusqu’à la version de spécification `2025-11-25`, et la génération moderne, `2026-07-28`. **[Versions du protocole](../protocol-versions.md)** est la page consacrée à cette séparation elle-même.
|
||||
|
||||
Cette page traite du côté serveur de cette séparation, et la réponse tient en une phrase : **le `streamable_http_app()` que vous déployez déjà sert les deux.**
|
||||
|
||||
Le SDK route chaque requête selon son en-tête `MCP-Protocol-Version`. Une requête qui indique `2026-07-28` va au gestionnaire (handler) moderne. Une requête qui indique une version de la génération poignée de main, ou qui ne porte aucun en-tête (c’est ainsi qu’arrive la requête `initialize` d’un client antérieur à 2026), va au transport que ces clients attendent : poignée de main `initialize`, sessions et tout le reste. Cela se fait requête par requête, avant votre code, sur la même et unique application.
|
||||
|
||||
Un client historique n’est donc pas quelque chose *pour* lequel vous construisez. C’est quelque chose qui se connecte *au* serveur que vous avez déjà écrit. Vous ne configurez rien.
|
||||
|
||||
!!! note
|
||||
Rien, littéralement. Il n’y a pas d’option `legacy=`, pas de liste de versions autorisées, aucun
|
||||
moyen de refuser ou de désactiver une génération : ni sur `streamable_http_app()`, ni sur `run()`,
|
||||
ni sur le gestionnaire de sessions. Les deux générations sont toujours actives. Ce qui se rapproche
|
||||
le plus d’un interrupteur par génération dans cette signature, c’est `stateless_http`, et il occupe
|
||||
l’essentiel de cette page.
|
||||
|
||||
## Un gestionnaire, deux générations {#one-handler-both-eras}
|
||||
|
||||
Voici un outil (tool) qui doit demander quelque chose à l’utilisateur, et des clients des deux générations qui l’appellent :
|
||||
|
||||
```python title="server.py" hl_lines="24 37-38"
|
||||
--8<-- "docs_src/legacy_clients/tutorial001.py"
|
||||
```
|
||||
|
||||
`reserve` a besoin d’une chose que le modèle n’a pas fournie : le nombre d’exemplaires. `Annotated[..., Resolve(ask_quantity)]` est la façon dont un outil le déclare (tous les détails sont dans **[Dépendances](../handlers/dependencies.md)**). Rien dans `reserve` ne nomme une version, ne vérifie une capacité ni ne bifurque.
|
||||
|
||||
Les deux clients sont ouverts **en même temps**, sur le même objet `mcp`. `mode="legacy"` exécute la poignée de main `initialize` : exactement la connexion qu’ouvre un client antérieur à 2026. L’autre prend la valeur par défaut et arrive en version `2026-07-28`.
|
||||
|
||||
```text
|
||||
2025-11-25 {'result': "Reserved 2 of 'Dune'."}
|
||||
2026-07-28 {'result': "Reserved 2 of 'Dune'."}
|
||||
```
|
||||
|
||||
Même serveur, même gestionnaire, même réponse. C’est toute la fonctionnalité.
|
||||
|
||||
Cela vaut la peine de s’arrêter sur le *comment*, car la même question a été posée aux deux clients sur deux liaisons complètement différentes. La connexion `2026-07-28` n’a aucun canal sur lequel le serveur puisse envoyer une requête ; `Resolve` a donc renvoyé la question dans le résultat de l’outil, et le client a relancé l’appel avec la réponse (**[Requêtes à plusieurs allers-retours (multi-round-trip)](../handlers/multi-round-trip.md)**). La connexion `2025-11-25` n’a rien de tel ; là, `Resolve` a envoyé une vraie requête `elicitation/create` en plein appel et a attendu. Vous n’avez écrit ni l’un ni l’autre. `Resolve` lit la version négociée de la connexion et choisit ; le corps de votre outil voit un `AcceptedElicitation` dans les deux cas.
|
||||
|
||||
!!! tip
|
||||
Cette portabilité entre générations est *la raison* pour laquelle `Resolve` est l’API sur laquelle
|
||||
construire. Son aînée `ctx.elicit()` (**[Élicitation](../handlers/elicitation.md)**) n’envoie jamais
|
||||
que `elicitation/create`, et ne fonctionne donc que sur une connexion historique. Sur une connexion
|
||||
`2026-07-28`, l’appel échoue. Si un outil l’utilise encore, le correctif est celui que vous voyez
|
||||
ci-dessus, pas une vérification de version.
|
||||
|
||||
## Ce que vous coûte une session historique {#what-a-legacy-session-costs-you}
|
||||
|
||||
Le routage est gratuit. La session ne l’est pas.
|
||||
|
||||
Une connexion `2026-07-28` est **sans session** : chaque requête est autonome, et le gestionnaire moderne n’émet jamais de `Mcp-Session-Id`. Une connexion historique, c’est l’inverse. Dès qu’un client antérieur à 2026 envoie `initialize`, le SDK crée un `Mcp-Session-Id`, le renvoie dans un en-tête de réponse et conserve derrière lui un enregistrement vivant que les requêtes ultérieures du client retrouveront : la version négociée, les flux ouverts, une tâche d’arrière-plan qui pilote la session.
|
||||
|
||||
Cet enregistrement est un **simple `dict` en mémoire du processus**. Il n’y a pas de magasin de sessions distribué, ni aucun moyen d’en brancher un.
|
||||
|
||||
Sur un seul worker, c’est invisible. Sur deux, c’est tout le problème : une requête qui porte un `Mcp-Session-Id` et atterrit sur un worker qui ne l’a pas créé ne trouve rien dans ce dict, et la réponse est un `404` (`Session not found`), pas le résultat de l’outil. Dès que vous exécutez plus d’un worker, **les clients historiques ont donc besoin d’un routage avec affinité (sticky routing)** : chaque requête d’une session doit atteindre le processus qui l’a démarrée. Les clients modernes, jamais ; ils n’ont aucune session à laquelle rester attachés. **[Déployer et passer à l’échelle](deploy.md)** couvre l’affinité et tout le reste sur l’exécution de plusieurs instances.
|
||||
|
||||
!!! warning
|
||||
`event_store=` ressemble au correctif et ne l’est pas. C’est la **reprise** (rejouer les
|
||||
événements SSE manqués pour un client qui se reconnecte à la *même* session), pas un magasin de
|
||||
sessions. Il ne rend jamais une session accessible depuis un autre processus.
|
||||
|
||||
## Le seul réglage : `stateless_http` {#the-one-knob-stateless_http}
|
||||
|
||||
Si l’affinité est un coût que vous refusez de payer, il y a exactement une chose que vous pouvez changer.
|
||||
|
||||
```python title="server.py" hl_lines="28"
|
||||
--8<-- "docs_src/legacy_clients/tutorial002.py"
|
||||
```
|
||||
|
||||
C’est le serveur du haut de la page, plus un mot-clé. `stateless_http=True` fait que la voie historique construit à la place une session jetable, propre à chaque requête : aucun `Mcp-Session-Id` émis, rien de mémorisé entre les requêtes, si bien que n’importe quel worker peut servir n’importe quelle requête et que le répartiteur de charge peut faire ce qu’il veut.
|
||||
|
||||
Deux choses à son sujet comptent plus que ce qu’il fait.
|
||||
|
||||
**Il ne touche que la voie historique.** Les requêtes sont routées sur l’en-tête de version *avant* que `stateless_http` ne soit lu, si bien que la voie moderne ne le voit jamais. Une connexion `2026-07-28` est déjà sans session et reste exactement la même quelle que soit la valeur.
|
||||
|
||||
**Il coûte les deux canaux serveur-vers-client sur cette voie.** Une session qui vit le temps d’un seul `POST` n’a aucun flux dans lequel le serveur puisse pousser une requête, ni aucun flux autonome dans lequel pousser des notifications. Toute requête à l’initiative du serveur lève `NoBackChannelError` : `ctx.elicit()`, les appels retirés d’échantillonnage (sampling) et de racines (roots) (**[Fonctionnalités obsolètes](../deprecated.md)**), et, oui, `Resolve` qui pose sa question à un client *historique*. Les notifications n’ont même pas droit à une erreur ; elles sont abandonnées silencieusement.
|
||||
|
||||
!!! note
|
||||
`json_response=True` n’est pas ce réglage, mais il prélève la moitié du même coût sur *chaque*
|
||||
session historique : un `POST` auquel on répond par un seul corps JSON n’a aucun flux pour le canal
|
||||
lié à la requête, si bien qu’un `ctx.elicit()` en cours de requête lève la même `NoBackChannelError`
|
||||
et que les notifications liées à la requête sont abandonnées. Le flux autonome de la session n’est
|
||||
pas touché : les notifications sans rapport arrivent toujours.
|
||||
|
||||
!!! check
|
||||
Faites la mauvaise chose. `reserve` est exactement l’outil qui vient de servir les deux clients.
|
||||
Déployez-le avec `stateless_http=True`, connectez les deux mêmes clients en HTTP et appelez-le
|
||||
depuis chacun.
|
||||
|
||||
Le client moderne obtient toujours `Reserved 2 of 'Dune'.` La voie moderne n’a pas changé.
|
||||
|
||||
L’appel du client historique ne revient pas sous la forme d’un résultat `is_error` que le modèle
|
||||
pourrait lire. La requête entière échoue, en erreur de protocole de premier niveau :
|
||||
|
||||
```text
|
||||
mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.
|
||||
```
|
||||
|
||||
`Resolve` ne vous a pas sauvé. Sur une connexion `2025-11-25`, il *doit* envoyer
|
||||
`elicitation/create`, et le canal dont il a besoin est exactement ce que `stateless_http=True` a
|
||||
abandonné. Un code portable entre générations n’est pas un code sans canal de retour (back-channel).
|
||||
|
||||
C’est donc un vrai compromis, et il n’existe que sur la voie historique : **avec session et affinité, ou sans état et à sens unique.** Si vos outils ne rappellent jamais le client, `stateless_http=True` est gratuit et vous devriez le prendre. S’ils le font, gardez les sessions et gardez le routage avec affinité.
|
||||
|
||||
## Où votre code bifurque réellement {#where-your-code-actually-forks}
|
||||
|
||||
Presque nulle part.
|
||||
|
||||
Outils, ressources, prompts, sortie structurée, progression, erreurs : aucun ne se soucie de la génération qui a appelé. La poignée de main `initialize`, le `Mcp-Session-Id`, le flux autonome, le `DELETE` qui met fin à une session : le SDK possède tout cela, et un gestionnaire n’en voit jamais rien. La saisie interactive est *le* seul endroit où les générations diffèrent véritablement sur la liaison, et `Resolve` existe pour que ce ne soit pas votre problème : vous venez de voir un seul outil servir les deux.
|
||||
|
||||
Il reste exactement une chose, et ce sont les **notifications de changement**, parce que les deux générations écoutent sur des tuyaux différents :
|
||||
|
||||
* Un client `2026-07-28` ouvre un flux `subscriptions/listen` et lit le bus des abonnements. `ctx.notify_resource_updated()` (et `notify_tools_changed()`, `notify_prompts_changed()`, `notify_resources_changed()`) y publient, et *seulement* là. **[Abonnements](../handlers/subscriptions.md)** est la page correspondante.
|
||||
* Un client historique lit le flux autonome que sa session garde ouvert. `ctx.session.send_resource_updated()` (et `send_tool_list_changed()` et consorts) écrivent sur la *connexion* qui a porté la requête : pour une session historique, c’est son flux autonome. Une connexion moderne n’a pas d’endroit pour cela : en HTTP, ce canal n’existe pas, et en stdio les quatre types de notifications de changement ne circulent que sur les flux `subscriptions/listen`, si bien que sur une connexion moderne la notification est discrètement abandonnée.
|
||||
|
||||
En HTTP, aucun des deux appels n’atteint les clients de l’autre génération. Pour prévenir tout le monde, appelez les deux :
|
||||
|
||||
```python title="server.py" hl_lines="19-20"
|
||||
--8<-- "docs_src/legacy_clients/tutorial003.py"
|
||||
```
|
||||
|
||||
Deux lignes, pas de `if`, pas de vérification de version, et c’est terminé. C’est la liste complète des choses qu’un gestionnaire fait différemment parce qu’un client historique existe.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* Un seul `streamable_http_app()` sert les deux générations de protocole. Le SDK route chaque requête selon son en-tête `MCP-Protocol-Version` ; il n’y a rien à configurer et aucun réglage de génération à chercher.
|
||||
* Un client historique vous coûte une session : un enregistrement `Mcp-Session-Id` en mémoire du processus, sans magasin distribué derrière. Plus d’un worker signifie **routage avec affinité**, sinon le mauvais worker répond `404 Session not found`. Tous les détails sur le multi-worker sont dans **[Déployer et passer à l’échelle](deploy.md)**.
|
||||
* `stateless_http=True` est le seul réglage, et il ne concerne **que la voie historique**. Il offre une répartition de charge gratuite aux clients historiques au prix des deux canaux serveur-vers-client sur cette voie : les requêtes à l’initiative du serveur lèvent `NoBackChannelError` (une erreur de premier niveau côté client, pas un résultat `is_error`), et les notifications sont abandonnées.
|
||||
* Une connexion `2026-07-28` est sans session dans tous les cas. `stateless_http` ne la touche jamais.
|
||||
* Le code de vos gestionnaires bifurque selon la génération à un seul endroit exactement : les notifications de changement. `ctx.notify_*` atteint les clients `subscriptions/listen` ; `ctx.session.send_*` atteint les sessions historiques. Appelez les deux.
|
||||
* Tout le reste (y compris demander une saisie à l’utilisateur, via `Resolve`) est portable entre générations par construction. Écrivez la version moderne une seule fois.
|
||||
112
i18n/fr/pages/run/opentelemetry.md
Normal file
112
i18n/fr/pages/run/opentelemetry.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
---
|
||||
translation:
|
||||
sections: [bc0227014724fa49, 15738c2f7fd67d86, a2c17bbe3f707e2f, d0d853376f162c06, b6368643fcc1c8d8, 902e33e17564a607]
|
||||
tool: 1
|
||||
---
|
||||
# OpenTelemetry {#opentelemetry}
|
||||
|
||||
Votre serveur est déjà tracé. Vous n’avez rien à ajouter.
|
||||
|
||||
Chaque serveur que vous créez émet un span [OpenTelemetry](https://opentelemetry.io/) pour chaque
|
||||
message qu’il traite. Vous ne l’avez pas écrit, et vous ne l’importez pas. Il est là dès l’instant où vous
|
||||
appelez `MCPServer(...)`.
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/opentelemetry/tutorial001.py"
|
||||
```
|
||||
|
||||
C’est un serveur complet, et tracé. Appelez `search_books` et un span est créé pour cet appel. Il en va de
|
||||
même pour le `Server` bas niveau : le traçage est présent sur les deux.
|
||||
|
||||
## Ce que vous obtenez {#what-you-get}
|
||||
|
||||
Chaque message entrant devient un span `SERVER` nommé d’après la méthode et sa cible. Ainsi, un
|
||||
`tools/call` pour `search_books` donne le span `tools/call search_books`, et un simple `tools/list`
|
||||
donne tout bonnement `tools/list`.
|
||||
|
||||
Chaque span porte quelques attributs :
|
||||
|
||||
* `mcp.method.name` et `mcp.protocol.version`, sur chaque span.
|
||||
* `jsonrpc.request.id`, sur une requête (une notification n’en a pas).
|
||||
* Un gestionnaire qui lève une exception passe le statut du span à erreur. Un résultat d’outil avec `is_error=True` aussi.
|
||||
|
||||
Et comme tracer un appel d’outil est un besoin très courant, les spans `tools/call` parlent les
|
||||
[conventions sémantiques GenAI](https://opentelemetry.io/docs/specs/semconv/gen-ai/) d’OpenTelemetry :
|
||||
|
||||
* `gen_ai.operation.name`, défini à `"execute_tool"`.
|
||||
* `gen_ai.tool.name`, défini au nom de l’outil appelé.
|
||||
|
||||
Un span `prompts/get` reçoit `gen_ai.prompt.name` dans le même esprit. Les méthodes de liste ne portent aucune
|
||||
clé `gen_ai.*`, car il n’y a rien à nommer.
|
||||
|
||||
!!! tip
|
||||
Ces attributs GenAI sont la raison pour laquelle une interface de traçage regroupe vos appels d’outils
|
||||
comme elle regroupe ceux de n’importe quel autre agent. Vous obtenez ce regroupement gratuitement, sans code supplémentaire.
|
||||
|
||||
## Cela ne coûte rien tant que vous n’en voulez pas {#it-costs-nothing-until-you-want-it}
|
||||
|
||||
Voici ce qui fait de « activé par défaut » une valeur par défaut confortable.
|
||||
|
||||
Le SDK ne dépend que de `opentelemetry-api`, la moitié légère d’OpenTelemetry. Sans SDK
|
||||
ni exportateur installé, créer un span est une opération vide. Les spans que votre serveur émet en ce
|
||||
moment même ne vous coûtent donc presque rien, et personne ne les collecte.
|
||||
|
||||
Le jour où vous voulez les *voir*, vous installez l’autre moitié et vous la pointez quelque part :
|
||||
|
||||
```console
|
||||
uv add opentelemetry-sdk opentelemetry-exporter-otlp
|
||||
```
|
||||
|
||||
Configurez un exportateur de la manière habituelle pour OpenTelemetry, et chaque span que le SDK
|
||||
créait discrètement s’allume. Le code de votre serveur ne change pas. Pas une ligne.
|
||||
|
||||
!!! info
|
||||
[Pydantic Logfire](https://logfire.pydantic.dev/) est l’un de ces backends, et il fait la
|
||||
configuration pour vous : `pip install logfire`, `logfire.configure()`, et vos spans MCP apparaissent
|
||||
dans la vue en direct. Il est construit sur OpenTelemetry, donc tout ce qui suit s’y applique aussi.
|
||||
|
||||
## Des traces qui traversent la liaison {#traces-that-cross-the-wire}
|
||||
|
||||
Une trace est surtout utile lorsqu’elle suit une requête du client jusque dans le serveur, en une
|
||||
seule image cohérente.
|
||||
|
||||
Lorsque le client et le serveur exécutent tous deux le SDK, ce lien est automatique. Le client injecte
|
||||
le [contexte de trace W3C](https://www.w3.org/TR/trace-context/) dans la requête, et le serveur
|
||||
le relit à l’arrivée, de sorte que le span serveur s’imbrique sous le span client dans la même trace. C’est la
|
||||
[SEP-414](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414), et vous l’obtenez sans
|
||||
rien demander.
|
||||
|
||||
Si le message entrant ne porte aucun contexte de trace, par exemple une requête provenant d’un client qui n’est pas
|
||||
le SDK, le span serveur se rattache simplement au span déjà courant côté serveur, au lieu
|
||||
de démarrer une toute nouvelle trace orpheline.
|
||||
|
||||
## Le désactiver {#turning-it-off}
|
||||
|
||||
Le traçage est un middleware, le premier de la liste de votre serveur. Si vous voulez vraiment un serveur qui
|
||||
n’émet aucun span, retirez-le :
|
||||
|
||||
```python
|
||||
from mcp.server._otel import OpenTelemetryMiddleware
|
||||
|
||||
mcp._lowlevel_server.middleware[:] = [
|
||||
m for m in mcp._lowlevel_server.middleware if not isinstance(m, OpenTelemetryMiddleware)
|
||||
]
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Cet import commence par un tiret bas, et c’est voulu. La classe est provisoire, de la
|
||||
même manière que [`Server.middleware`](../advanced/middleware.md) est provisoire : attendez-vous donc
|
||||
à ce que le chemin d’import change. Vous n’en avez presque jamais besoin : sans exportateur installé, les spans
|
||||
sont gratuits, et la réponse habituelle consiste donc à les laisser activés et à ne pas installer d’exportateur.
|
||||
|
||||
## Récapitulatif {#recap}
|
||||
|
||||
* Chaque `MCPServer` et chaque `Server` bas niveau émet un span `SERVER` par message entrant, par
|
||||
défaut. Vous n’écrivez rien.
|
||||
* Les spans portent `mcp.method.name` et `mcp.protocol.version` ; `tools/call` et `prompts/get` portent
|
||||
aussi des attributs GenAI, pour que vos appels d’outils se regroupent comme ceux de n’importe quel autre agent.
|
||||
* Cela ne coûte rien tant que vous n’installez pas un SDK OpenTelemetry et un exportateur, puis tout s’allume
|
||||
sans aucune modification de votre serveur.
|
||||
* Le contexte de trace du client vers le serveur se propage automatiquement lorsque les deux côtés exécutent le SDK.
|
||||
|
||||
Ce qui décide si une requête s’exécute ou non, c’est l’**[Autorisation](authorization.md)**.
|
||||
Loading…
Reference in a new issue