On vient de migrer le serveur MCP d'Energy Data Platform. Deux changements en un : on a retiré FastMCP au profit du SDK officiel version 2, et on est passé à la révision 2026-07-28 du protocole, qui supprime la session. Voici ce que ça veut dire concrètement, et ce que ça a coûté.
MCP en une phrase
Le Model Context Protocol est la façon standard de donner des outils à un modèle. Au lieu de laisser Claude inventer un tarif réglementé, on lui expose une fonction lookup_tarif ; il l'appelle, un serveur répond avec la vraie valeur, et le modèle rédige à partir de ce résultat. Le serveur MCP, c'est le programme qui héberge ces outils.
Chez nous, ce serveur expose les calculs métier de l'énergie : simulation tarifaire, lecture des grilles en vigueur, analyse de facture. Le chatbot ne calcule jamais lui-même, c'est la règle qui rend ses réponses auditables.
Trois transports en trois ans
Pour comprendre ce qui change, il faut distinguer trois étapes, souvent confondues.
- 2024, HTTP+SSE. Deux points d'entrée : un
GET /ssequi laissait un flux ouvert en permanence pour que le serveur puisse pousser des messages, et desPOST /messagesséparés pour envoyer les demandes. - 2025, Streamable HTTP. Un seul point d'entrée
/mcp. Le client commence par un handshakeinitialize, le serveur peut lui attribuer un identifiant de session renvoyé dans l'en-têteMcp-Session-Id, et chaque requête suivante le reporte. Voir la spécification des transports 2025. - 2026-07-28. Le handshake et la session de protocole disparaissent. Chaque requête est autonome. Voir l'annonce officielle.
D'où l'on vient
Notre serveur était sur la révision 2025, pas sur l'ancien transport 2024. Le schéma ci-dessous compare donc 2025 et 2026-07-28, ce qui est la vraie bascule que nous avons vécue.Ce qu'était une session, et ce qu'elle coûtait
En 2025, le serveur pouvait se souvenir. Se souvenir de qui avait fait initialize, de l'état de négociation des capacités, des abonnements en cours. Trois conséquences, qu'il faut énoncer sans les exagérer :
- Le passage à l'échelle se complique. Deux instances derrière un répartiteur, et un appel qui atterrit sur la mauvaise ne retrouve pas sa session. On s'en sort avec de l'affinité de session ou un magasin de sessions partagé, mais le basculement en cas de panne devient nettement moins simple.
- Un redémarrage invalide les sessions MCP. Pas nécessairement la conversation métier, qui vit ailleurs, en base. Le client doit refaire
initialize, ce qui est récupérable, mais qu'il faut avoir prévu. - Il y a un état à entretenir, donc à expirer, à nettoyer, et à surveiller quand des clients disparaissent sans fermer proprement.
Ce que change la révision 2026-07-28
Il n'y a plus de handshake ni d'identifiant de session. Chaque requête porte elle-même ce qu'il faut pour être traitée : la version du protocole, la méthode, le nom de l'outil, et les informations client dans _meta.
Sur le fil, avant
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{ ... }}
--- reponse ---
HTTP/1.1 200 OK
Mcp-Session-Id: 8f3a1c7e-4b21-4d9a-9f0c-2e7d5a1b3c44
--- puis chaque appel reporte la session ---
POST /mcp HTTP/1.1
Mcp-Session-Id: 8f3a1c7e-4b21-4d9a-9f0c-2e7d5a1b3c44
{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"lookup_tarif","arguments":{"puissance_kva":36}}}Sur le fil, après
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: lookup_tarif
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"lookup_tarif",
"arguments":{"puissance_kva":36},
"_meta":{"client":{"name":"edp-chatbot","version":"1.4.0"}}}}Plus de handshake préalable, plus d'en-tête de session. La méthode et le nom de l'outil remontent dans les en-têtes, ce qui permet au passage de router ou de journaliser sans avoir à ouvrir le corps de la requête.
Ce que stateless_http=True fait vraiment
La révision 2026-07-28 est sans état par construction : ce n'est pas un drapeau qui la rend telle. Dans le SDK Python,stateless_http=True configure principalement le comportement servi aux clients restés en 2025 et avant, pour qu'ils soient traités sans que le serveur conserve leur session. Voir le guide de déploiement du SDK Python.Le diff réel
- """EDP MCP Server — FastMCP with tools, resources, auth, and audit logging."""
+ """EDP MCP Server — official MCP SDK with tools and resources."""
- from fastmcp import FastMCP
+ from mcp.server import MCPServer
- mcp = FastMCP("edp-mcp-server")
+ mcp = MCPServer("edp-mcp-server", version="0.2.0")
- mcp.run(transport="streamable-http", host=host, port=port)
+ mcp.run(transport="streamable-http", host=host, port=port, stateless_http=True)- "fastmcp>=0.3.0",
- "mcp>=0.10.0",
+ "mcp>=2,<3",Pourquoi FastMCP est sorti, la vraie raison
Ce n'est pas un procès en obsolescence. La version que nous utilisions, FastMCP 3.2.4, imposait mcp>=1.24,<2. Elle nous bloquait donc sur le SDK v1, et empêchait mécaniquement le passage au SDK Python v2 dont nous avions besoin pour la révision 2026.
Depuis, FastMCP 4 prend en charge MCP 2026. Rester sur FastMCP était donc redevenu possible. Notre décision de le retirer est un choix de simplification architecturale : une couche de moins entre notre code et le protocole, une implémentation au lieu de deux. Ce n'est pas une critique du projet, qui a rendu de vrais services pendant un an.
Effet mesuré sur le verrou de dépendances : 401 lignes supprimées, 64 ajoutées, soit 337 lignes nettes en moins.
Ce qu'on gagne concrètement
- N'importe quelle instance répond à n'importe quel appel. Plus besoin d'affinité ni de magasin de sessions partagé pour ajouter un serveur.
- Le basculement en cas de panne devient trivial. Il n'y a rien à répliquer entre instances.
- Un déploiement ne demande plus de renégociation. Attention toutefois : les appels en vol pendant le redémarrage échouent quand même, le client doit les rejouer. Ce qui disparaît, c'est l'étape de renégociation, pas l'échec transitoire.
- Une dépendance de moins, donc une surface de mise à jour et de sécurité réduite d'autant.
Sur la rétrocompatibilité
Le mode servi aux anciens clients couvre les opérations principales, celles dont nous avons besoin : lister les outils, les appeler, lire des ressources. Certaines fonctions historiques peuvent en revanche être limitées, notamment le canal retour du serveur vers le client, la reprise d'un flux interrompu et les notifications. Si votre intégration en dépend, vérifiez avant de basculer.
Les pièges
Le rejeu, et les effets de bord en double
C'est le piège le plus sérieux, et il arrive avec le sans état. Puisqu'un appel peut échouer en vol et que le client est encouragé à le rejouer, un même appel peut être exécuté deux fois. Sur une lecture, c'est sans conséquence. Sur une écriture, ça l'est beaucoup moins.
La parade : rendre les outils idempotents, ou faire porter à l'appelant une clé d'idempotence que le serveur mémorise le temps nécessaire pour reconnaître un doublon. À surveiller en priorité sur les opérations de paiement, de suppression, d'envoi et de création.
L'état applicatif, à ne pas confondre avec la session
Ce qui disparaît, c'est la session de protocole. Les données métier restent en base : grilles tarifaires, dossiers clients, historique des conversations.
Le réflexe à éviter, c'est de compenser en envoyant tout l'historique de conversation à un outil. C'est coûteux, bruyant, et ça expose des données dont l'outil n'a pas besoin. La bonne pratique est de passer des identifiants explicites que le serveur sait résoudre : dossier_id, basket_id, browser_id. L'outil va chercher ce qu'il lui faut, et rien d'autre.
L'authentification
Une remarque qu'on lit souvent et qui est trompeuse : « il faut maintenant authentifier chaque requête ». C'était déjà le cas. En HTTP, chaque requête doit porter et faire vérifier ses justificatifs ; une session MCP n'a jamais été un cache d'authentification. Ce qui change, c'est qu'on n'a plus la tentation de s'appuyer dessus. Notre journal d'audit enregistre désormais l'appel plutôt que la session, ce qui est plus verbeux et nettement plus utile quand il faut prouver qui a demandé quoi.