9 min de lecture
MCP en production : ouvrir un produit aux assistants IA sans rien casser
Ce que nous avons appris en exposant un produit à ChatGPT, Claude et Mistral avec un serveur MCP : une règle de parité avec l'interface, peu d'outils, des erreurs que le modèle sait corriger, et une autorisation OAuth 2.1 solide.
Par Sahid Orentino Ferdjaoui
Pourquoi un connecteur MCP
Les professionnels qui utilisent notre produit travaillent déjà avec un assistant IA toute la journée. Plutôt que de leur demander d'ouvrir une application, nous leur permettons de dire ce qu'ils veulent — et de laisser l'assistant le faire.
Crée une commande pour le client Martin, 12 rue des Lilas, 78000 Versailles, et génère-moi le lien de paiement.
C'est fait : la commande est créée en brouillon dans votre compte, et voici le lien de paiement à transmettre au client. Des options payantes existent ; je peux en ajouter une si vous le souhaitez.
MCP est un protocole ouvert fondé sur JSON-RPC. Un serveur y déclare des outils : chacun a un nom, une description et un schéma d'entrée (un JSON Schema). Le client — l'assistant — les liste avec tools/list, puis les appelle avec tools/call. Deux transports sont définis : stdio pour les outils locaux, et Streamable HTTP pour un service distant, ce qui nous concerne.
Une règle : la parité avec l'interface
Le piège d'un connecteur est d'ouvrir un second chemin vers votre produit, avec ses propres règles, donc ses propres bugs. Nous avons posé une règle unique :
Le connecteur ne fait rien que le parcours de l'interface ne fasse déjà.
Concrètement, une commande passée par un assistant crée le même brouillon, applique les mêmes valeurs par défaut, fige le même prix, génère le même lien et envoie le même courriel qu'un parcours lancé dans l'interface. Elle est indiscernable d'une commande normale, et le professionnel peut l'ouvrir dans l'interface pour la terminer. La seule trace de l'origine est un événement d'historique qui nomme l'assistant.
L'intérêt dépasse la cohérence. Un oubli le montre : dans l'interface, le prix est figé implicitement à une certaine étape. Le premier jet du connecteur sautait cette étape — et la page de paiement aurait affiché 0 €. Un chemin parallèle qui copie la logique oublie toujours quelque chose ; un chemin qui appelle la même logique ne le peut pas.
Peu d'outils, bien définis
Nous exposons quatre outils, ni plus ni moins :
| Outil | Rôle |
|---|---|
create_order | Crée une commande en brouillon, avec le minimum d'informations |
update_order | Modifie un brouillon en place |
send_payment_link | Envoie le lien de paiement quand l'adresse e-mail arrive après la commande |
list_orders | Suit les commandes, avec recherche et filtre de statut |
Une définition d'outil ressemble à ceci :
{
"name": "create_order",
"description": "Crée une commande en brouillon dans le compte de l'utilisateur et renvoie le lien de paiement.",
"inputSchema": {
"type": "object",
"properties": {
"address": { "type": "string", "description": "Adresse concernée" },
"zipcode": { "type": "string", "description": "Code postal, 5 chiffres" },
"city": { "type": "string" },
"customer": { "type": "object", "description": "Nom ou raison sociale" }
},
"required": ["address", "zipcode", "city", "customer"]
}
}Quatre règles de conception sont revenues sans cesse :
- Un champ par déclaration. Pas de champ « commentaire » fourre-tout : le modèle y verserait tout, et l'information serait perdue pour le reste du système. Chaque chose qu'on peut dire a sa place, comme dans l'interface.
- Le minimum bloquant, des défauts pour le reste. Seules les informations réellement indispensables sont obligatoires. Le résultat contient une liste
notesde tout ce qui a été supposé : rien n'est silencieux. - Modifier plutôt que recommencer. Lors d'un premier essai, l'assistant a créé une seconde commande pour changer une option, et a glissé une information dans le champ commentaire, faute de champ adapté. Le brouillon doit se modifier en place : d'où
update_order, qui ne change que les champs donnés. - Signaler, jamais imposer. Les options payantes ne sont jamais ajoutées d'office. Quand la commande est créée sans option, le résultat liste celles qui existent, avec leur prix : l'assistant les mentionne une fois, sans insister.
Des erreurs que le modèle sait corriger
La spécification MCP distingue deux familles d'erreurs. Les erreurs de protocole (outil inconnu, requête mal formée) sont des erreurs JSON-RPC classiques. Les erreurs d'exécution sont renvoyées dans le résultat avec isError: true : la spécification précise qu'elles doivent porter un retour actionnable, que le modèle utilise pour se corriger et réessayer.
Notre règle découle directement de cette distinction :
- un défaut que l'assistant peut corriger (un nom manquant, un code postal invalide, une option refusée par l'abonnement) revient en
isError, avec un message en français que l'assistant relaie ; - tout le reste est journalisé côté serveur et renvoyé sous la forme d'un message générique : aucun détail interne ne sort.
{
"result": {
"isError": true,
"content": [
{ "type": "text", "text": "Le code postal « 7800 » n'est pas valide : 5 chiffres sont attendus." }
]
}
}Valider une entrée floue : trois verdicts
L'interface propose une saisie assistée. Un assistant, lui, recopie ce que l'utilisateur a dit. Il nous fallait donc valider les adresses nous-mêmes, auprès d'un géocodeur public qui renvoie, pour chaque candidat, un score de 0 à 1.
Nous avons mesuré ce score sur des requêtes réelles :
- une adresse exacte obtient 0,97 et plus ;
- une adresse abrégée ou mal orthographiée (« bd », « fbg st ») obtient 0,64 à 0,77, avec la bonne rue en tête ;
- une adresse fausse (une rue qui n'existe pas dans cette ville) obtient 0,50 à 0,67, avec la mauvaise rue en tête.
Les deux bandes du milieu se recouvrent : aucun seuil ne sait dire « abrégée » d'un côté et « fausse » de l'autre. Plutôt que de choisir un seuil et d'assumer des erreurs dans les deux sens, nous avons ajouté un troisième verdict :
| Verdict | Quand | Ce que fait l'outil |
|---|---|---|
| Trouvée | meilleur candidat ≥ 0,8, même code postal, même ville | crée la commande |
| Douteuse | tout le reste | refuse (isError) et propose jusqu'à trois adresses proches |
| Non vérifiée | le géocodeur n'a pas répondu en 4 secondes | crée la commande, avec une note |
Deux principes, au-delà de l'adresse. D'abord, une commande n'attend jamais un tiers : si le service est indisponible, on crée et on le signale. Ensuite, c'est un verdict, pas une correction : l'adresse saisie n'est jamais réécrite — un « Bât. A » ou un « 4 bis » n'appartient pas au géocodeur. L'assistant fait choisir une suggestion à l'utilisateur, ou lui fait confirmer l'adresse telle quelle (address_confirmed) pour les cas légitimes, comme un bâtiment neuf.
Transport : sans état, en JSON
Streamable HTTP permet de répondre à une requête POST en JSON ou en flux d'événements, et la gestion de session est facultative. Nos outils sont de simples requêtes / réponses : nous avons donc choisi un point d'entrée sans état, qui ne répond qu'en JSON, sans flux d'événements et sans identifiant de session. Il tourne tel quel sur l'API existante et se met à l'échelle comme elle.
Le point d'entrée implémente initialize (en négociant la version du protocole : une version inconnue reçoit la plus récente), ping, tools/list et tools/call ; les notifications reçoivent un 202.
Chaque requête exige un jeton. Sans jeton, ou avec un jeton expiré ou révoqué, la réponse est un 401 avec un en-tête WWW-Authenticate qui pointe vers les métadonnées de la ressource : c'est ainsi qu'un assistant découvre la procédure d'autorisation à partir de la seule adresse collée par l'utilisateur. Le compte et l'organisation sont relus à chaque appel : quelqu'un qui a quitté son organisation depuis son consentement est refusé aussitôt.
Autorisation : OAuth 2.1
Pourquoi pas une simple clé d'API ? Elle est réservée à certains abonnements, elle ne règle pas la question de l'identité de la personne, et surtout les connecteurs des grands assistants s'authentifient par OAuth. La spécification MCP s'appuie sur OAuth 2.1 et sur trois RFC : les métadonnées de serveur d'autorisation (RFC 8414), les métadonnées de ressource protégée (RFC 9728) et l'enregistrement dynamique de clients (RFC 7591).
Le parcours est entièrement automatique pour l'utilisateur, qui colle une adresse puis clique sur Autoriser. Nos choix principaux :
- PKCE S256 uniquement, pour tous les clients : un code intercepté ne sert à rien sans le vérificateur.
- URI de redirection strictes : en https, ou en http sur la boucle locale seulement (clients de bureau et en ligne de commande), avec correspondance exacte. Un client inconnu ou une URI non enregistrée reçoit l'erreur sur la page et n'est jamais redirigé ; tout autre défaut revient à l'assistant par la redirection, avec le paramètre
iss(RFC 9207). - Le nom de l'assistant sur la page de consentement est celui qu'il a enregistré, jamais un nom lu dans l'URL : on n'autorise pas un nom que quelqu'un d'autre a pu choisir.
- Jetons opaques, pas de JWT. Un jeton
<identifiant>.<secret>, dont seul le hachage est stocké. Un jeton signé avec la clé du front ouvrirait aussi les routes du front ; un jeton opaque se vérifie par une lecture, et supprimer l'autorisation révoque tous les jetons qu'elle a émis. - Durées de vie courtes :
| Élément | Durée | Règle |
|---|---|---|
| Code d'autorisation | 10 minutes | usage unique, brûlé à la première présentation |
| Jeton d'accès | 1 heure | |
| Jeton de rafraîchissement | 90 jours | renouvelé à chaque usage : l'ancien meurt |
Un connecteur utilisé reste donc connecté ; un connecteur inactif depuis trois mois doit être autorisé à nouveau.
- Limitation de débit adaptée : les appelants sont les serveurs des assistants, dont les adresses sont partagées par tous leurs utilisateurs. Le quota est donc libéré après chaque succès, et seule une source qui échoue sans cesse s'accumule.
Révoquer, et expliquer
Une page des réglages liste les assistants autorisés par le compte. Révoquer supprime l'autorisation : ses jetons cessent de fonctionner à l'instant, et l'assistant doit être réautorisé pour revenir. La page de consentement indique où la trouver.
Pour aider à connecter chaque assistant, nous publions un guide pas à pas par assistant. Ces menus changent — l'un d'eux a déplacé son « mode développeur » deux fois en un an — nous traitons donc les étapes comme des données, avec la date de leur dernière vérification, plutôt que comme du texte écrit en dur.
Ce qu'il reste à faire
- Plafonner les créations par compte : rien n'empêche aujourd'hui un assistant coincé dans une boucle de créer des brouillons à l'infini. Ils ne coûtent rien tant qu'ils ne sont pas payés, mais ils encombrent.
- Rafraîchissement concurrent : si deux détenteurs d'une même autorisation rafraîchissent en même temps, le second reçoit
invalid_grant. Nous ne l'avons pas observé ; un délai de grâce sur l'ancien jeton serait la réponse.
Tester
L'inspecteur MCP lance lui-même tout le parcours :
npx @modelcontextprotocol/inspectorPointez-le sur l'adresse de votre serveur. À la main, le parcours est : enregistrer un client, ouvrir la page d'autorisation, échanger le code contre un jeton, puis appeler initialize, tools/list et tools/call avec ce jeton.