Les événements MCP permettent à votre serveur MCP de pousser des mises à jour vers ChatGPT dès qu'elles se produisent, de sorte qu'aucun agent n'ait à les interroger. Depuis l'OpenAI DevDay le 29 septembre 2026, ChatGPT prend en charge la spécification proposée des événements MCP à la version de protocole 2026-07-28 (MCP 2.0), sur tous les forfaits. Votre serveur ajoute trois méthodes (events/list, events/subscribe, events/unsubscribe), annonce events dans server/discover, passe un défi de vérification de rappel, et signe chaque livraison avec HMAC de Standard Webhooks. ChatGPT accepte uniquement la livraison par webhook.
Ce guide couvre chaque format de message documenté par OpenAI, les règles de sécurité que votre serveur applique, et un parcours de test pour la boucle complète. Nouveau dans le protocole ? Commencez par ce qu'est le MCP. Vous enverrez les appels JSON-RPC et simulerez le rappel dans Apidog.
Événements MCP en un coup d'œil
| Élément | Ce que ChatGPT attend |
|---|---|
| Protocole | MCP 2.0, version 2026-07-28 |
| Capacité | "events": {} dans les capacités de server/discover |
| Méthodes | events/list, events/subscribe, events/unsubscribe, sur le même point d'accès authentifié que vos outils |
| Livraison | Webhook uniquement : pas de sondage, de streaming, ou de notifications gap ou terminated |
| Signature | HMAC-SHA256 de Standard Webhooks |
| En-têtes | webhook-id, webhook-timestamp, webhook-signature, X-MCP-Subscription-Id |
| Secret | whsec_ plus base64 qui se décode en 24-64 octets, fourni par ChatGPT |
| Limite de charge utile | 256 KiB (262 144 octets), un événement par requête |
| ID d'abonnement | Déterministe : principal, URL de rappel, nom d'événement, arguments |
| Rappels | HTTPS, vérifié par défi, pas d'adresses privées, pas de redirections |
Sources : le guide des événements MCP d'OpenAI et l'esquisse de conception des événements MCP.
Pourquoi des événements au lieu du sondage
Sans événements, un agent surveillant les commentaires de révision appelle un outil à intervalles réguliers et compare les résultats, consommant des requêtes lorsque rien n'a changé et accusant un retard lorsque quelque chose l'a fait. Les événements MCP inversent la direction : votre serveur sait quand un enregistrement change, il envoie donc la mise à jour. Les compromis sont ceux classiques des webhooks versus le sondage, désormais intégrés au MCP.
Le récapitulatif du DevDay d'OpenAI utilise un tableau de projet : l'utilisateur demande à ChatGPT de surveiller les nouvelles tâches, et quand l'une d'elles arrive, ChatGPT lit les documents liés et élabore un plan. Les documents ajoutent deux cas supplémentaires : transformer les rapports de bugs d'un canal en demandes de pull (message.created, filtrées par channel_id), et appliquer les commentaires de révision à un document (comment.created, filtrés par document_id).
La spécification provient du groupe de travail Triggers et Événements du MCP (dépôt) et est étiquetée expérimentale, alors fixez votre code à 2026-07-28. Pour le reste du lancement, consultez le hub du DevDay 2026.
Annoncez les événements et définissez-les
Ajoutez events aux capacités de votre réponse server/discover :
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": { "tools": {}, "events": {} }
}
}
Retournez ensuite les définitions depuis events/list. Chaque définition comprend un name, une description, delivery: ["webhook"], un inputSchema pour les arguments d'abonnement (vos filtres, tels que document_id), et un payloadSchema pour l'objet data dans chaque livraison. Utilisez des noms stables et des descriptions spécifiques, appliquez des filtres sur votre serveur, et ne listez que les événements que le compte connecté peut voir.
events/subscribe : requête et réponse
Lorsqu'un utilisateur demande à ChatGPT de surveiller quelque chose, ChatGPT appelle events/subscribe :
{
"jsonrpc": "2.0",
"id": 2,
"method": "events/subscribe",
"params": {
"name": "comment.created",
"arguments": { "document_id": "doc_123" },
"delivery": {
"mode": "webhook",
"url": "https://receiver.example.com/mcp-events/callback_123",
"secret": "whsec_<base64-encoded-signing-key>"
},
"cursor": null
}
}
Avant d'accepter, autorisez l'utilisateur pour cet événement et ces arguments, validez les deux par rapport à votre définition, exigez un secret whsec_ dont le base64 se décode en 24-64 octets, vérifiez le rappel (section suivante), et stockez l'abonnement avec son propriétaire, ses filtres, son URL, son secret et son expiration. Répondez ensuite :
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"id": "sub_123",
"refreshBefore": "2026-10-02T12:00:00Z",
"cursor": null,
"truncated": false
}
}
Trois règles assurent la correction des abonnements :
- ID déterministes. Dérivez l'
iddu principal authentifié, de l'URL de rappel, du nom de l'événement et des arguments. L'esquisse de conception suggère un SHA-256 tronqué de cette clé. - Upserts idempotents. Un abonnement répété avec la même identité met à jour l'enregistrement existant. Comparez les arguments comme un JSON canonique afin que l'ordre des clés ne puisse pas créer de doublons.
- Actualisation avant
refreshBefore. ChatGPT rappelleevents/subscribeavant cette heure avec la même identité et son dernier curseur enregistré ; vous retournez une nouvelle expiration. Si une actualisation contient un nouveau secret, remplacez-le et signez avec les deux clés pendant une courte période. Retournezcursor: nullpour les événements que vous ne pouvez pas rejouer.
Vérifiez le rappel avant toute donnée
Avant d'envoyer des données d'application, POSTez un corps de vérification signé avec un défi frais, à usage unique et de courte durée :
{ "type": "verification", "challenge": "a-single-use-random-value" }
Donnez-lui un webhook-id unique (par exemple msg_verification_123) et les mêmes en-têtes de signature qu'une livraison. ChatGPT renvoie {"challenge": "a-single-use-random-value"} avec un 2xx. Comparez en temps constant, puis activez la livraison. En cas d'échec, retournez l'erreur JSON-RPC -32015 (CallbackEndpointError) avec un data.reason tel que challenge_failed ou timeout. Mettez en cache une réussite par principal et par URL pour une période limitée afin que les actualisations ne relancent pas de défi.
Le défi existe parce que l'abonné fournit le secret : sans lui, n'importe qui pourrait diriger votre serveur vers l'URL d'une victime et l'inonder. Appliquez le même renforcement à chaque requête sortante : HTTPS uniquement, résolvez et validez l'adresse au moment de la connexion, bloquez les adresses privées, locales et autres non publiques, et ne suivez jamais les redirections.
Livrez et signez les événements
Lorsqu'un événement correspondant se produit, POSTez un objet d'événement à l'URL de rappel :
{
"eventId": "evt_456",
"name": "comment.created",
"timestamp": "2026-10-01T12:05:00Z",
"data": {
"document_id": "doc_123",
"comment_id": "comment_456",
"text": "Pouvons-nous ajouter les dates de déploiement à cette section ?",
"url": "https://docs.example.com/doc_123#comment_456"
},
"cursor": null
}
Envoyez Content-Type: application/json, webhook-id (égal à eventId), webhook-timestamp (secondes Unix au moment de la signature), webhook-signature, et X-MCP-Subscription-Id. Sérialisez le corps une seule fois et envoyez ces octets exacts, car la signature les couvre.
Restez en dessous de 256 KiB ; pour les enregistrements volumineux, envoyez un résumé et exposez un outil de lecture. Traitez le texte rédigé par l'utilisateur comme des données et n'incluez jamais d'instructions de modèle dans une charge utile. Réessayez les échecs transitoires avec une temporisation exponentielle limitée, en conservant l'ID d'événement et en signant chaque tentative avec une nouvelle signature. Ne réessayez pas les 410 ou 413. Les événements peuvent arriver dans le désordre, alors rendez les outils d'écriture idempotents. Le guide de conception de webhooks fiables couvre les planifications de réessai.
Vérifier une signature Standard Webhooks
Conformément à la spécification Standard Webhooks, le contenu signé est ${webhook-id}.${webhook-timestamp}.${body}, la clé HMAC-SHA256 est le secret décodé en base64 après avoir retiré whsec_, et l'en-tête contient une ou plusieurs signatures v1,<base64> séparées par des espaces. L'ébauche MCP indique aux récepteurs de rejeter les horodatages plus anciens que 5 minutes et de dédupliquer sur webhook-id.
// receiver.mjs : récepteur Standard Webhooks strict pour les tests locaux (Node 18+)
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.WEBHOOK_SECRET; // whsec_...
const MAX_BYTES = 256 * 1024;
const TOLERANCE_S = 5 * 60;
const seen = new Set();
export function verify(raw, h, secret, now = Math.floor(Date.now() / 1000)) {
const id = h["webhook-id"], ts = h["webhook-timestamp"], sigs = h["webhook-signature"];
if (!id || !ts || !sigs) return false;
const t = Number(ts);
if (!Number.isInteger(t) || Math.abs(now - t) > TOLERANCE_S) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${ts}.`).update(raw).digest();
return sigs.split(" ").some((s) => {
const [version, b64] = s.split(",");
const got = Buffer.from(b64 ?? "", "base64");
return version === "v1" && got.length === expected.length && timingSafeEqual(got, expected);
});
}
if (SECRET) createServer((req, res) => {
const chunks = [];
let size = 0;
req.on("data", (c) => { size += c.length; if (size <= MAX_BYTES) chunks.push(c); });
req.on("end", () => {
if (size > MAX_BYTES) return res.writeHead(413).end();
const raw = Buffer.concat(chunks);
if (!verify(raw, req.headers, SECRET)) return res.writeHead(401).end();
let body;
try { body = JSON.parse(raw); } catch { return res.writeHead(400).end(); }
if (body.type === "verification") {
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ challenge: body.challenge }));
}
const id = req.headers["webhook-id"];
if (!seen.has(id)) { seen.add(id); console.log(req.headers["x-mcp-subscription-id"], body.name, id); }
res.writeHead(200).end();
});
}).listen(8787);
Exécutez-le avec WEBHOOK_SECRET=whsec_... node receiver.mjs. Il renvoie 413 au-delà de 256 KiB et 401 pour une signature mauvaise ou périmée, renvoie les défis et enregistre chaque webhook-id une seule fois. Nous avons vérifié verify() par rapport au vecteur de test de signature de la bibliothèque JavaScript Standard Webhooks. Pour la théorie, lisez la vérification de signature de webhook.
Votre serveur bloque les adresses privées, il ne livrera donc pas à localhost par défaut. L'ébauche autorise les cibles non publiques uniquement lorsqu'elles sont explicitement configurées : ajoutez une entrée à la liste blanche réservée aux développeurs, ou exposez le récepteur via un tunnel.
Testez-le avant ChatGPT
Voici un parcours de test Apidog pour les modes d'échec listés par OpenAI. Créez un environnement avec MCP_URL, MCP_TOKEN, CALLBACK_URL et WEBHOOK_SECRET, et envoyez chaque appel JSON-RPC comme un POST vers {{MCP_URL}} avec Authorization: Bearer {{MCP_TOKEN}} plus les en-têtes requis par la liaison HTTP Streamable : Accept: application/json, text/event-stream, MCP-Protocol-Version: 2026-07-28, et Mcp-Method défini sur la méthode. Chaque corps a également besoin de params._meta avec io.modelcontextprotocol/protocolVersion et io.modelcontextprotocol/clientCapabilities. Les exemples d'OpenAI l'omettent, et un _meta manquant renvoie également -32602, donc l'étape 3 passerait pour une mauvaise raison.
- Découverte. Envoyez
server/discoveret affirmez que$.result.capabilities.eventsexiste et que$.result.supportedVersionscontient2026-07-28. Envoyezevents/listet affirmez quedeliveryde chaque événement inclutwebhook. - Abonnement idempotent. Envoyez le corps d'abonnement ci-dessus en utilisant
{{CALLBACK_URL}}et{{WEBHOOK_SECRET}}, avec un post-processeur d'extraction de variable qui enregistre$.result.idcommeSUB_ID. Renvoyez-le inchangé, puis avec les clésargumentsréordonnées, et affirmez que$.result.idest égal à{{SUB_ID}}les deux fois. - Validation. Envoyez un secret qui se décode en moins de 24 octets, puis un rappel
http://. L'ébauche associe les deux à-32602(InvalidParams) ; affirmez sur$.error.code. Un rappel avec IP privée doit également échouer. - Défi de vérification. Créez un point de terminaison de simulation Apidog qui répond
{"challenge": "wrong"}et abonnez-vous avec son URL de simulation cloud comme rappel. Affirmez que$.error.codeest-32015et que$.error.data.reasonestchallenge_failed. Ensuite, pointezCALLBACK_URLvers votre récepteur et confirmez que l'abonnement réussit. - Charge utile trop grande. Déclenchez un événement dont le corps dépasse 262 144 octets. Votre expéditeur devrait le refuser ; si l'un d'eux passe, le récepteur répond
413, et les journaux de votre serveur devraient montrer une seule tentative, pas une nouvelle tentative. - Relecture et altération. Copiez les en-têtes et le corps d'une livraison signée depuis le journal de sortie de votre serveur dans une nouvelle requête Apidog. Renvoyez-la immédiatement :
200, et pas de deuxième ligne de journal. Changez un octet du corps :401. Renvoyez après 5 minutes :401.
Enregistrez les étapes comme un scénario et exécutez-le en CI avec l'interface CLI d'Apidog. Le manuel de test de serveur MCP couvre les appels d'outils, et comment tester les webhooks approfondit les récepteurs. Connectez ensuite le serveur à ChatGPT via un plugin et exécutez la liste de contrôle du cycle de vie d'OpenAI.
FAQ
Que sont les événements MCP ? Une extension MCP expérimentale qui permet à un serveur de pousser des notifications d'événements à un client au lieu que le client ne les interroge. ChatGPT prend en charge son mode webhook à 2026-07-28.
ChatGPT prend-il en charge le sondage ou le streaming pour les événements MCP ? Non. ChatGPT prend en charge uniquement la livraison par webhook et la vérification de rappel. Le sondage, le streaming, et les notifications gap ou terminated ne sont pas pris en charge.
Quels forfaits ChatGPT bénéficient des événements MCP ? Le récapitulatif du DevDay d'OpenAI indique que la fonctionnalité est disponible pour tous les forfaits.
Qui crée le secret de signature ? L'abonné. ChatGPT envoie un secret whsec_ dans delivery.secret ; votre serveur le valide, le stocke et signe avec lui, et ne génère jamais le sien.
En quoi cela diffère-t-il de l'API Agents ? Les événements MCP poussent les données de votre serveur vers ChatGPT. L'API OpenAI Agents exécute les agents que vous construisez et rapporte leur progression par streaming ou par webhooks.
Prochaine étape
Ajoutez "events": {} à un serveur, exposez un événement avec un filtre, et exécutez les six vérifications contre un récepteur local avant de connecter ChatGPT. Téléchargez Apidog pour conserver ces vérifications sous forme de scénario exécuté à chaque commit.
