Événements MCP expliqués : créer et tester un serveur MCP émetteur de webhooks pour ChatGPT

Explication des événements MCP : comment ChatGPT s'abonne à votre serveur MCP via /events/subscribe, la signature standard des webhooks, la limite de 256 Kio et les tests dans Apidog.

Medy Evrard

2 October 2026

Événements MCP expliqués : créer et tester un serveur MCP émetteur de webhooks pour ChatGPT

Apidog pour les entreprises

Déploiement sur site

SSO & RBAC

Conforme SOC 2

Découvrir Apidog Enterprise

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.

bouton

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 :

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.

  1. Découverte. Envoyez server/discover et affirmez que $.result.capabilities.events existe et que $.result.supportedVersions contient 2026-07-28. Envoyez events/list et affirmez que delivery de chaque événement inclut webhook.
  2. 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.id comme SUB_ID. Renvoyez-le inchangé, puis avec les clés arguments réordonnées, et affirmez que $.result.id est égal à {{SUB_ID}} les deux fois.
  3. 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.
  4. 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.code est -32015 et que $.error.data.reason est challenge_failed. Ensuite, pointez CALLBACK_URL vers votre récepteur et confirmez que l'abonnement réussit.
  5. 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.
  6. 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.

Pratiquez le Design-first d'API dans Apidog

Découvrez une manière plus simple de créer et utiliser des API