La révision 2026-07-28 du Model Context Protocol est présentée par ses mainteneurs comme la plus importante depuis la publication initiale du protocole. Ce n’est pas une exagération marketing : elle retire du cœur du protocole les deux mécanismes qui obligeaient tout serveur MCP à maintenir un état par connexion. Concrètement, un serveur MCP devient une application HTTP comme une autre — et cela change à peu près tout dans la façon de le déployer, de le sécuriser et de le tester.
Cet article résume ce qui change, pourquoi c’est structurant côté infrastructure, et comment migrer du code existant sans casser les agents qui en dépendent.
Note de lecture : les noms exacts des clés
_metaet la forme précise des schémas évoluent encore entre les révisions du document et les SDK. Vérifiez systématiquement la spécification et le changelog officiel avant de figer une implémentation.
Ce qui change en une page
Le principe directeur de la révision est simple : chaque requête doit être auto-suffisante. Toutes les informations nécessaires au traitement — version du protocole négociée, capacités du client, identité, contexte d’autorisation — voyagent avec la requête, et non dans un état côté serveur établi lors d’une phase préalable.
Les conséquences en cascade :
- disparition du handshake
initialize/notifications/initialized; - disparition de l’en-tête
Mcp-Session-Iddu transport HTTP streamable ; - déplacement de la négociation de version et des capacités client dans le champ
_metade chaque requête ; - introduction d’en-têtes HTTP de routage (
Mcp-Method,Mcp-Name) ; - remplacement des interactions initiées par le serveur (elicitation, sampling) par un modèle de Multi Round-Trip Requests ;
- promotion de l’extension Tasks au statut officiel pour le travail de longue durée ;
- résultats de listes explicitement cacheables ;
- durcissement du volet autorisation.
Adieu le handshake
Dans les révisions antérieures, un client devait ouvrir la conversation par initialize, recevoir les capacités du serveur, puis confirmer avec notifications/initialized. Le serveur en déduisait un état de session, souvent matérialisé par un Mcp-Session-Id renvoyé dans les en-têtes de réponse et rejoué par le client à chaque appel suivant.
Ce modèle disparaît. La version du protocole et les capacités du client sont désormais transportées dans _meta, par requête :
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "search_invoices",
"arguments": { "customer": "ACME", "year": 2026 },
"_meta": {
"io.modelcontextprotocol/protocol-version": "2026-07-28",
"io.modelcontextprotocol/client-capabilities": {
"tasks": {},
"roots": { "listChanged": true }
}
}
}
}
Côté serveur, cela signifie que la logique « si non initialisé, refuser » n’a plus de raison d’être. Elle est remplacée par une validation locale : la version annoncée est-elle supportée ? Les capacités déclarées permettent-elles la stratégie de réponse que je veux appliquer (par exemple : puis-je renvoyer un handle de tâche, ou dois-je répondre en synchrone) ?
Pourquoi c’est important au-delà du protocole
Le vrai gain est opérationnel. Avec un Mcp-Session-Id, une session était de fait épinglée à l’instance qui avait traité le handshake. Il fallait donc :
- des sticky sessions au niveau du load balancer, ou un store partagé (Redis, base) pour rendre l’état déplaçable ;
- des drains prudents au déploiement, sous peine de couper des conversations en cours ;
- un scale-to-zero difficile à assumer.
Sans état de session, un serveur MCP devient déployable sur du serverless (Lambda, Cloud Run, Azure Functions) et sur de l’edge (Cloudflare Workers, Deno Deploy). Le routage redevient du round-robin trivial, les rolling updates ne cassent plus rien, et l’autoscaling suit réellement la charge. Pour beaucoup d’équipes qui hébergeaient un serveur MCP sur un conteneur long-lived uniquement à cause du handshake, la facture d’infrastructure et la complexité opérationnelle chutent.
Les nouveaux en-têtes Mcp-Method et Mcp-Name
Deux en-têtes exposent au niveau HTTP l’intention de la requête, sans obliger l’infrastructure à parser le corps JSON-RPC :
POST /mcp HTTP/1.1
Host: mcp.exemple.fr
Content-Type: application/json
Authorization: Bearer eyJhbGciOi...
Mcp-Method: tools/call
Mcp-Name: search_invoices
C’est un détail qui a de grandes conséquences pratiques. Un API gateway, un WAF, un service mesh ou un reverse proxy peut appliquer du rate limiting différencié (tools/call plus coûteux que tools/list), du routage par outil, de l’autorisation grossière ou de l’observabilité par nom d’outil — en configuration déclarative, sans lambda d’inspection de payload.
Exemple de règle NGINX :
map $http_mcp_method $mcp_zone {
default "read";
"tools/call" "write";
"tasks/update" "write";
}
limit_req_zone $binary_remote_addr$mcp_zone zone=mcp:10m rate=20r/s;
Attention : ces en-têtes sont des métadonnées de transport, pas une source de vérité. Le serveur doit continuer à autoriser sur la base du corps JSON-RPC réellement exécuté. Un client malveillant qui annonce Mcp-Name: harmless_tool et appelle autre chose dans le corps ne doit jamais franchir la couche applicative.
Multi Round-Trip Requests : la fin des appels initiés par le serveur
L’elicitation (demander une information à l’utilisateur) et le sampling (demander une complétion au modèle du client) reposaient sur une capacité structurellement incompatible avec le stateless : le serveur initiait une requête vers le client au milieu du traitement.
Le nouveau modèle inverse le flux. Le serveur termine sa réponse avec un résultat de type InputRequiredResult, qui décrit ce dont il a besoin, et le client relance l’appel en fournissant l’information :
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"type": "input_required",
"inputRequest": {
"kind": "elicitation",
"message": "Confirmez le compte à débiter.",
"requestedSchema": {
"type": "object",
"properties": { "accountId": { "type": "string" } },
"required": ["accountId"]
}
},
"continuation": "eyJzdGVwIjoyLCJvcmRlciI6Ijk5MSJ9"
}
}
Le jeton de continuation encapsule le contexte nécessaire à la reprise. Deux règles à respecter absolument :
- Signez et chiffrez ce jeton (JWE, ou opaque avec HMAC) : il traverse un client non fiable.
- Donnez-lui une durée de vie courte et un usage unique, pour éviter le rejeu d’une étape de workflow.
C’est un changement de conception réel : votre outil ne peut plus être une fonction bloquante qui « pose une question ». Il devient une petite machine à états dont chaque transition est un appel HTTP indépendant.
L’extension Tasks devient officielle
Le corollaire du stateless, c’est qu’on ne peut plus tenir une requête ouverte pendant dix minutes derrière un gateway serverless. L’extension Tasks, jusqu’ici expérimentale, devient le mécanisme officiel du travail de longue durée : le serveur répond immédiatement avec un handle, et le client pilote le cycle de vie via tasks/get, tasks/update et tasks/cancel.
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"task": {
"taskId": "tsk_01JQ8W...",
"status": "working",
"pollInterval": 2000
}
}
}
Le pattern est celui, classique, de l’opération asynchrone HTTP : l’état vit dans votre backing store (file, base, workflow engine), pas dans le process. Respectez le pollInterval suggéré côté client, et exposez des états terminaux non ambigus pour que l’agent sache s’arrêter.
Résultats de listes cacheables
tools/list, resources/list et prompts/list sont désormais explicitement cacheables, avec la sémantique HTTP habituelle :
HTTP/1.1 200 OK
Cache-Control: max-age=300, public
ETag: "tools-v17"
Sur un agent qui redécouvre les capacités à chaque tour de boucle, l’effet est immédiat : moins d’appels facturés, moins de cold starts inutiles, latence de premier token réduite. En contrepartie, la révocation d’un outil devient un problème d’invalidation de cache. Faites varier l’ETag sur toute modification du catalogue, et gardez des TTL courts si votre surface d’outils dépend des droits de l’appelant — un catalogue mis en cache pour le mauvais tenant est une fuite de données, pas une optimisation.
Durcissement de l’autorisation
Deux évolutions notables :
- Validation de l’issuer selon la RFC 9207. Le client doit vérifier le paramètre
issrenvoyé dans la réponse d’autorisation et le comparer à l’issuer attendu, ce qui ferme les attaques de type mix-up entre serveurs d’autorisation. - Sortie progressive du Dynamic Client Registration (RFC 7591) au profit des client metadata documents (CIMD) : le client publie ses métadonnées à une URL stable que le serveur d’autorisation récupère, au lieu de créer dynamiquement des enregistrements côté serveur. Cela supprime une surface d’abus bien connue (spam d’enregistrements, clients éphémères non traçables).
Si vous opérez un serveur MCP protégé, l’essentiel reste inchangé et non négociable : validez l’audience du token, refusez les tokens émis pour un autre resource server, et n’acceptez jamais un token « passé au travers » depuis un client tiers.
Extensions et politique de dépréciation
La révision formalise un cadre d’extensions nommées, déclarées via les capacités, et une politique de dépréciation explicite : une fonctionnalité est d’abord marquée dépréciée, reste supportée pendant une fenêtre annoncée, puis est retirée. En pratique, cela change la façon de lire la compatibilité : au lieu de raisonner « je supporte la version X », on raisonne « je supporte la version X plus tel jeu d’extensions ». Vos tests d’intégration devraient refléter cette matrice, et non une seule version figée.
Nouvelle surface d’attaque
Le stateless supprime des classes de bugs et en crée d’autres. Trois points de vigilance :
- Rejeu. Sans session, il n’y a plus de contexte implicite qui invalide une requête ancienne. Les jetons de continuation et les handles de tâches doivent porter leur propre expiration et, idéalement, un nonce à usage unique.
- Autorisation par requête. Un contrôle effectué « au moment du handshake » n’existe plus. Chaque appel doit revalider le token, les scopes et l’appartenance au tenant. C’est plus de travail par requête — d’où l’intérêt d’un cache de validation JWKS local.
- Cache empoisonné. Les listes cacheables introduisent la question du
Vary. Si le catalogue dépend de l’identité, il ne doit pas être mis en cache par un intermédiaire partagé sans clé de cache incluant cette identité.
Guide de migration pas à pas
- Auditez toute dépendance à
Mcp-Session-Idet, plus largement, à un état implicite.grep -r "sessionId\|Mcp-Session-Id"est un bon point de départ, mais cherchez aussi les Map en mémoire indexées par session, les compteurs par connexion, les caches d’authentification par socket. - Remplacez l’état implicite par un handle explicite. Tout ce qui survivait entre deux appels devient soit un jeton de continuation signé, soit une tâche persistée, soit une ressource identifiée par URI.
- Traquez les codes d’erreur codés en dur, en particulier
-32002. Selon les implémentations, il a servi à signaler « ressource introuvable » ou « serveur non initialisé / session inconnue ». La seconde sémantique n’a plus de sens : un client qui la traite comme « je dois refaire le handshake » entrera en boucle. Nettoyez les deux côtés. - Migrez depuis l’API Tasks expérimentale vers la forme officielle : noms de méthodes, forme du handle, états. Prévoyez une période où votre serveur accepte les deux, le temps que les clients suivent.
- Réécrivez elicitation et sampling en
InputRequiredResult. C’est le chantier le plus coûteux, car il touche la logique métier de vos outils. - Négociez la version côté serveur, sans supposer. Un client ancien qui envoie encore
initializedoit recevoir une réponse intelligible — soit une compatibilité maintenue le temps de la transition, soit une erreur explicite qui nomme la version attendue.
Exemple : un serveur MCP stateless en fonction serverless
L’ensemble du serveur tient dans un handler sans état, ce qui était impossible auparavant :
// worker.ts — déployable sur Cloudflare Workers, Lambda (via adapter), Cloud Run…
const SUPPORTED = new Set(["2026-07-28"]);
export default {
async fetch(req: Request, env: Env): Promise<Response> {
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
// 1. Autorisation revalidée à CHAQUE requête.
const claims = await verifyAccessToken(req.headers.get("authorization"), env);
if (!claims) return new Response("Unauthorized", { status: 401 });
const rpc = await req.json<JsonRpcRequest>();
const meta = rpc.params?._meta ?? {};
const version = meta["io.modelcontextprotocol/protocol-version"];
if (!SUPPORTED.has(version)) {
return json(rpcError(rpc.id, -32600, `Version non supportée : ${version}`));
}
switch (rpc.method) {
case "tools/list":
// 2. Liste cacheable, clé de cache dépendante du tenant.
return json(toolsFor(claims.tenant), {
"Cache-Control": "private, max-age=300",
"ETag": catalogEtag(claims.tenant),
"Vary": "Authorization",
});
case "tools/call":
return json(await callTool(rpc, claims, env));
case "tasks/get":
return json(await env.TASKS.get(rpc.params.taskId, claims.tenant));
default:
return json(rpcError(rpc.id, -32601, "Méthode inconnue"));
}
},
};
Aucune variable globale mutable, aucun état de session : deux requêtes successives peuvent atterrir sur deux isolats différents sans conséquence.
Mesurer l’effet de la migration
Ne prenez pas le gain pour acquis : mesurez-le sur votre charge. Un protocole de test honnête compare l’ancien déploiement (conteneur + sticky sessions) et le nouveau (fonction stateless) sur le même scénario applicatif :
# scenario.js : 1 tools/list puis 5 tools/call par utilisateur virtuel
k6 run --vus 200 --duration 5m scenario.js
Les métriques qui comptent réellement :
- p50 / p95 / p99 de latence par méthode (
tools/listettools/calln’ont pas le même profil) ; - taux de cold start et latence du premier appel après scale-to-zero ;
- nombre de requêtes réellement servies depuis le cache après activation de l’
ETag; - coût par million d’appels, cold starts inclus ;
- comportement pendant un rolling update : avec l’ancien modèle, comptez les sessions cassées ; avec le nouveau, cette métrique doit tomber à zéro.
Documentez ces chiffres pour votre charge : c’est le seul argument qui tiendra face à une équipe plateforme.
Le passage au stateless n’est pas une simplification cosmétique du protocole : c’est un déplacement de responsabilité. Ce que MCP gérait implicitement pour vous — la continuité d’une conversation — devient un artefact que vous concevez, signez, expirez et stockez. Les serveurs qui s’en sortiront le mieux sont ceux dont la logique métier était déjà exprimée comme une suite de transitions explicites plutôt que comme un long appel bloquant. Pour les autres, la migration d’elicitation et de sampling vers InputRequiredResult sera l’occasion — inconfortable mais utile — de rendre visible un état qui n’aurait jamais dû être implicite.
Comments (0)
Please log in to leave a comment.
Log InNo comments yet. Be the first to comment!