La révision du Model Context Protocol du 28 juillet 2026 change le fonctionnement des MCP distants. Le cœur est sans état et fondé sur des requêtes indépendantes, donc un équilibreur peut envoyer l’appel suivant vers une autre instance. Elle ajoute server/discover, MRTR, des en-têtes obligatoires, des indications de cache, des extensions et une autorisation renforcée. Les détails normatifs viennent de l’annonce officielle et du journal des changements.
Cette évolution concerne l’état du protocole, pas l’état métier. Un outil peut toujours utiliser une base de données ou un workflow durable. Seul l’état caché lié à une session de transport MCP disparaît.
Ce qui change par rapport au protocole de 2025
L’ancien cycle de vie commençait par une requête initialize, suivie de la notification notifications/initialized. Avec Streamable HTTP, le serveur pouvait aussi fournir Mcp-Session-Id. Il associait ensuite les messages à cette connexion. En 2026-07-28, l’échange initialize et l’en-tête de session protocolaire sont supprimés. Chaque requête indique la version du protocole et les capacités du client dans _meta. Le client devrait aussi fournir io.modelcontextprotocol/clientInfo, tandis que le serveur devrait identifier son implémentation dans les métadonnées du résultat.
Voici une comparaison utile pour préparer la migration :
| Domaine | Comportement 2025 | Comportement 2026-07-28 |
|---|---|---|
| Cycle de vie | initialize et notifications/initialized |
Aucun handshake protocolaire |
| Session | Mcp-Session-Id HTTP facultatif |
Aucune session au niveau du protocole |
| Capacités | Négociées une fois | Déclarées à chaque requête |
| Découverte | Après initialize ou par convention | server/discover obligatoire côté serveur moderne, appel client facultatif |
| Serveur vers client | Requêtes sur un canal maintenu | MRTR renvoie les demandes dans la réponse |
| Routage HTTP | Le proxy analyse souvent le JSON | En-têtes Mcp-Method et Mcp-Name selon le cas |
| Listes et lectures | Fraîcheur définie par le client | Indications ttlMs et cacheScope |
| Reprise | SSE pouvait utiliser des identifiants d’événements | Pas de reprise Last-Event-ID, nouvelle requête nécessaire |
| Enregistrement | DCR souvent utilisé automatiquement | CIMD préféré, DCR conservé pour compatibilité |
La révision déplace Tasks dans io.modelcontextprotocol/tasks, remplace l’ancien flux par subscriptions/listen et déprécie Roots, Sampling, Logging ainsi que HTTP+SSE. La politique prévoit au moins douze mois, mais les nouvelles implémentations ne devraient pas les adopter.
Ce que signifie stateless en pratique
Avec Streamable HTTP moderne, le serveur expose un seul point MCP qui accepte POST. Le client envoie une requête ou une notification JSON-RPC par POST. La réponse est soit un objet JSON, soit un flux SSE limité à cette requête. Le serveur ne crée pas d’identifiant de session et un flux interrompu ne possède pas d’historique reprenable. En HTTP, fermer le flux de réponse signale l’annulation.
Les en-têtes de transport et le corps décrivent la même opération. Voici un appel d’outil minimal :
POST /mcp HTTP/1.1
Accept: application/json, text/event-stream
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search","arguments":{"q":"otters"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"io.modelcontextprotocol/clientInfo":{"name":"catalog-app","version":"1.0.0"}}}}
La valeur de MCP-Protocol-Version doit être identique à celle de _meta. Un serveur moderne rejette un en-tête obligatoire absent ou incohérent avec HTTP 400 et le code HeaderMismatch, -32020. Il doit vérifier les en-têtes après le passage du proxy. Un intermédiaire ne peut donc pas router vers un outil tandis que l’application en exécute un autre.
Le caractère stateless modifie la mise à l’échelle, pas le sens métier. Si un workflow doit continuer, renvoyez un handle explicite et exigez-le dans l’appel suivant. Conservez l’état dans une base ou un service de workflow, liez le handle à l’utilisateur et imposez une expiration. Un handle ou requestState non vérifié ne prouve jamais une autorisation.
server/discover et compatibilité des versions
Tout serveur moderne doit implémenter le RPC server/discover. Son résultat annonce les versions et capacités prises en charge, ainsi que des instructions facultatives. Le client peut l’appeler en premier pour sélectionner une version ou envoyer directement une requête moderne. Discovery est donc utile, mais ne constitue pas un handshake obligatoire côté client.
Si la version demandée n’est pas prise en charge, le serveur renvoie UnsupportedProtocolVersionError avec les versions disponibles. Le client choisit une version commune et réessaie. Un client compatible avec les deux époques doit classifier soigneusement sa sonde. Un 400 vide ou sans erreur JSON-RPC moderne reconnue peut signaler un ancien point d’entrée. Une erreur moderne reconnue signifie qu’il faut corriger la requête ou négocier à nouveau. Une erreur d’authentification ou d’infrastructure ne prouve pas que le serveur est ancien.
Le serveur peut conserver une route legacy. Ne déduisez pas l’époque d’une simple connexion TCP ou d’un 404 générique.
MRTR remplace les requêtes du serveur vers le client
Le format moderne supprime le canal JSON-RPC de serveur vers client. Un outil qui nécessite une confirmation, une valeur manquante ou une étape assistée par modèle renvoie un résultat intermédiaire au lieu de maintenir un flux ouvert. Le résultat porte resultType: "input_required" et une carte inputRequests. Le client répond, puis répète la méthode d’origine avec inputResponses. Cette répétition est une nouvelle requête et peut atteindre une autre réplique.
Un flux de confirmation peut avoir cette forme :
{
"resultType": "input_required",
"inputRequests": {
"confirm": {
"type": "elicitation",
"message": "Delete three files?",
"schema": {"type": "boolean"}
}
},
"requestState": "signed-opaque-state"
}
Le client envoie ensuite inputResponses.confirm et renvoie requestState octet par octet. Le serveur doit réentrer dans le gestionnaire comme pour une nouvelle requête. Rendez ce gestionnaire idempotent, déduisez l’étape du workflow depuis l’état vérifié et ne demandez que les données encore absentes. Une action destructive ne doit pas être considérée comme terminée avant la validation de la confirmation.
requestState n’est pas un conteneur sécurisé. Il passe par le client et doit être traité comme une donnée contrôlée par un attaquant. Signez-le avec HMAC ou utilisez un chiffrement authentifié, liez-le au principal, à la méthode originale, aux paramètres importants et à une expiration, puis rejetez toute altération avant l’exécution du gestionnaire. Une signature ne masque pas le contenu, donc n’y placez aucun secret. Le SDK TypeScript fournit un codec d’état de requête et un hook de vérification. Son shim legacy peut transformer le même gestionnaire input_required en anciennes requêtes elicitation/create, sampling/createMessage et roots/list pendant la prise en charge des clients 2025.
Pour les opérations longues, utilisez Tasks avec un handle durable, tasks/get et tasks/update.
Indications de cache et catalogues déterministes
Les résultats modernes de tools/list, prompts/list, resources/list, resources/templates/list et resources/read contiennent ttlMs et cacheScope. ttlMs est une indication de fraîcheur non négative en millisecondes, analogue à max-age HTTP. Zéro signifie que le résultat est immédiatement périmé. Pour un ancien serveur, une valeur absente doit être traitée comme zéro. Une valeur positive indique quand le client peut éviter une nouvelle lecture, sans garantir que les données resteront inchangées. Vérifiez la fraîcheur lorsque les données sont nécessaires, sans transformer TTL en polling continu.
La clé de cache doit inclure la méthode et chaque paramètre qui influence le résultat, notamment l’URI de ressource et le curseur d’une liste paginée. Ne mettez pas en cache une réponse contenant inputResponses ou requestState, car son contexte ne figure pas dans une clé de liste simple. cacheScope: "public" autorise le partage entre contextes d’autorisation. Utilisez-le seulement pour des données sans éléments propres à l’utilisateur ou aux permissions. L’autorisation par outil reste nécessaire même si le catalogue est en cache.
Le serveur devrait renvoyer les outils dans un ordre déterministe. Cet ordre stabilise les prompts et favorise la réutilisation du cache. Une notification listChanged complète TTL, mais ne remplace pas une indication correcte.
OAuth et sécurité
L’autorisation est facultative dans MCP. Un serveur HTTP qui protège ses ressources doit suivre le profil OAuth 2.1 de la spécification 2026. Il agit comme resource server et doit publier les métadonnées OAuth Protected Resource selon RFC 9728. Une réponse 401 devrait les indiquer avec WWW-Authenticate et un challenge de scopes utile. Les clients doivent accepter l’URL du header ainsi que les deux formes well-known.
Ces métadonnées peuvent identifier plusieurs authorization servers. Les clients doivent prendre en charge OAuth Authorization Server Metadata selon RFC 8414 et OpenID Connect Discovery.
L’enregistrement privilégie désormais les Client ID Metadata Documents. Un client préenregistré reste valide. Dynamic Client Registration est un mécanisme de repli déprécié. Lorsqu’il est utilisé, envoyez le bon application_type pour un client desktop ou CLI. Vérifiez PKCE, utilisez S256, enregistrez les redirect URI exactes et utilisez HTTPS, sauf pour un callback localhost autorisé.
Mémorisez l’issuer validé avec la transaction PKCE. Si la réponse contient iss, comparez-le avant d’échanger le code. Les identifiants sont liés à cet issuer et ne doivent pas être réutilisés ailleurs. Envoyez l’URI canonique du serveur MCP comme resource RFC 8707 dans les requêtes d’autorisation et de jeton. Le serveur doit vérifier l’audience.
Avec Streamable HTTP, vérifiez Origin pour bloquer le DNS rebinding. Un serveur local devrait écouter sur localhost plutôt que sur toutes les interfaces. Ne placez pas les bearer tokens dans les query strings ou les logs. Demandez le consentement avant d’exposer une ressource privée ou d’appeler un outil, et considérez descriptions et annotations comme non fiables si le serveur n’est pas de confiance. 401 signifie une autorisation absente ou invalide. 403 signifie des permissions insuffisantes et devrait inclure insufficient_scope lorsque c’est possible.
Migration du SDK TypeScript
Le SDK TypeScript v2 sépare les paquets client, serveur, cœur et runtime. Consultez le guide SDK pour 2026-07-28 et le guide de migration v1 vers v2. Une mise à jour seule ne fait pas nécessairement passer des octets modernes. Le client v2 négocie par défaut le mode legacy, activez donc versionNegotiation.
Pour un client compatible avec les deux époques, la forme documentée est la suivante :
import { Client } from '@modelcontextprotocol/client';
const client = new Client(
{ name: 'catalog-app', version: '1.0.0' },
{ versionNegotiation: { mode: 'auto' } },
);
await client.connect(transport);
mode: 'auto' sonde server/discover et revient au handshake 2025 seulement avec un pair réellement legacy. Épinglez 2026-07-28 si le fallback risque de masquer une incompatibilité. createMcpHandler(factory) construit un serveur HTTP moderne par requête et peut servir les deux époques. Pour choisir l’époque en stdio, utilisez serveStdio(() => buildServer()).
Remplacez l’enregistrement des gestionnaires v1 fondé sur des schémas par des chaînes de méthode, par exemple setRequestHandler('tools/call', handler). Remplacez la lecture de ctx.sessionId par des handles applicatifs ou par un requestState vérifié. Remplacez l’elicitation push par inputRequired(...). Une requête moderne n’envoie aucune notification de log sans io.modelcontextprotocol/logLevel. Le codemod du SDK est une aide mécanique, pas un test de compatibilité.
Pour valider, faites passer createMcpHandler par fetch et gardez une couverture du handshake legacy. Vérifiez headers, _meta, idempotence, cache, audience et statut HTTP avec des clients anciens et modernes.
Checklist de migration
- Inventoriez clients, serveurs, transports, stockages de sessions, event stores SSE et lectures de
Mcp-Session-Id. - Choisissez le point d’entrée du trafic moderne et celui qui conservera temporairement le trafic legacy.
- Mettez à jour le SDK et verrouillez les versions réelles dans le lockfile.
- Ajoutez
server/discoveret une politique de négociation des versions. - Rendez chaque requête autonome et validez
_metaainsi que les headers miroir. - Remplacez l’état par session par des handles ou un
requestStatesigné et expirant. - Réécrivez les interactions serveur en MRTR et rendez les répétitions sûres.
- Ajoutez
ttlMs,cacheScopeet un ordre déterministe aux catalogues et ressources. - Mettez à jour passerelles, WAF, métriques et traces pour
Mcp-MethodetMcp-Name. - Implémentez les vérifications issuer, resource, audience, PKCE, redirect, Origin et scopes.
- N’adoptez pas HTTP+SSE, Roots, Sampling, Logging ou DCR dans le nouveau code.
- Déployez progressivement avec observabilité, comparez les erreurs modernes et legacy, puis supprimez la compatibilité après migration des consommateurs.
Dépannage
| Symptôme | Cause probable | Action |
|---|---|---|
HTTP 400 avec -32020 |
En-tête absent ou différent du corps | Recalculez MCP-Protocol-Version, Mcp-Method et Mcp-Name depuis un même objet |
| HTTP 400 et erreur de version | Le pair ne sert pas la révision demandée | Choisissez une version de supported ou la route legacy |
| HTTP 404 et method-not-found | Le point d’entrée est moderne, mais la méthode est absente | Vérifiez méthode et extension, ne lancez pas initialize à l’aveugle |
| HTTP 401 ou 403 lors de discovery | La sonde est bloquée par l’authentification | Corrigez credentials et métadonnées, ce statut ne prouve pas legacy |
| Aucune notification de log | io.modelcontextprotocol/logLevel manque |
Activez-le par requête ou utilisez stderr et OpenTelemetry |
| Effets de bord répétés | Le flux ne peut plus être repris | Utilisez des idempotency keys et un nouvel ID de requête |
| Données utilisateur dans un autre cache | Résultat marqué public par erreur |
Passez à private et gardez les contrôles du principal |
| Ancien client reçoit 405 sur GET | Il attend HTTP+SSE | Gardez provisoirement une route legacy |
Pour le contexte d’architecture, consultez production AI agent architecture et MCP server with TypeScript and OAuth.
Sources
Ce guide suit la spécification MCP 2026-07-28, son journal des changements et des dépréciations, les exigences Streamable HTTP, SEP-2575 sur MCP sans état, l’annonce de la version 2026-07-28, la spécification d’autorisation MCP et la documentation de migration du SDK TypeScript.