Los Eventos MCP permiten que tu servidor MCP envíe actualizaciones a ChatGPT en el momento en que ocurren, para que ningún agente tenga que sondearlas. Desde el OpenAI DevDay el 29 de septiembre de 2026, ChatGPT es compatible con la especificación propuesta de Eventos MCP en la versión de protocolo 2026-07-28 (MCP 2.0), en todos los planes. Tu servidor añade tres métodos (events/list, events/subscribe, events/unsubscribe), anuncia events en server/discover, supera un desafío de verificación de devolución de llamada, y firma cada entrega con HMAC de Webhooks Estándar. ChatGPT solo acepta la entrega por webhook.
Esta guía cubre cada forma de mensaje que documenta OpenAI, las reglas de seguridad que impone tu servidor y una pasada de prueba para todo el ciclo. ¿Eres nuevo en el protocolo? Empieza con qué es MCP. Enviarás las llamadas JSON-RPC y simularás la devolución de llamada en Apidog.
Eventos MCP de un vistazo
| Elemento | Lo que espera ChatGPT |
|---|---|
| Protocolo | MCP 2.0, versión 2026-07-28 |
| Capacidad | "events": {} en las capacidades de server/discover |
| Métodos | events/list, events/subscribe, events/unsubscribe, en el mismo endpoint autenticado que tus herramientas |
| Entrega | Solo webhook: sin sondeo, transmisión, ni notificaciones de gap o terminated |
| Firma | HMAC-SHA256 de Webhooks Estándar |
| Encabezados | webhook-id, webhook-timestamp, webhook-signature, X-MCP-Subscription-Id |
| Secreto | whsec_ más base64 que decodifica a 24-64 bytes, suministrado por ChatGPT |
| Límite de carga útil | 256 KiB (262.144 bytes), un evento por solicitud |
| ID de Suscripción | Determinista: principal, URL de devolución de llamada, nombre del evento, argumentos |
| Devoluciones de llamada | HTTPS, verificación por desafío, sin direcciones privadas, sin redirecciones |
Fuentes: la guía de Eventos MCP de OpenAI y el borrador de esquema de diseño de Eventos MCP.
Por qué eventos en lugar de sondeo
Sin eventos, un agente que observa los comentarios de revisión llama a una herramienta en un temporizador y compara los resultados, quemando solicitudes cuando nada cambia y retrasándose cuando algo sí lo hace. Los Eventos MCP invierten la dirección: tu servidor sabe cuándo cambia un registro, por lo que envía la actualización. Las compensaciones son las clásicas de webhooks vs. sondeo, ahora dentro de MCP.
El resumen del DevDay de OpenAI utiliza un tablero de proyectos: el usuario le pide a ChatGPT que esté atento a nuevas tareas, y cuando llega una, ChatGPT lee los documentos vinculados y redacta un plan. La documentación añade dos casos más: convertir informes de errores en un canal en borradores de solicitudes de extracción (message.created, filtrado por channel_id), y aplicar comentarios de revisión a un documento (comment.created, filtrado por document_id).
La especificación proviene del Grupo de Trabajo de Triggers y Eventos de MCP (repositorio) y está etiquetada como experimental, así que fija tu código a 2026-07-28. Para el resto del lanzamiento, consulta el centro DevDay 2026.
Anuncia eventos y defínelos
Añade events a las capacidades en tu respuesta de server/discover:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": { "tools": {}, "events": {} }
}
}
Luego, devuelve las definiciones de events/list. Cada una incluye un name, una description, delivery: ["webhook"], un inputSchema para los argumentos de suscripción (tus filtros, como document_id), y un payloadSchema para el objeto data en cada entrega. Usa nombres estables y descripciones específicas, aplica filtros en tu servidor y lista solo los eventos que la cuenta conectada pueda ver.
events/subscribe: solicitud y respuesta
Cuando un usuario le pide a ChatGPT que supervise algo, ChatGPT llama a 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
}
}
Antes de aceptar, autoriza al usuario para ese evento y esos argumentos, valida ambos contra tu definición, requiere un secreto whsec_ cuyo base64 decodifique a 24-64 bytes, verifica la devolución de llamada (sección siguiente), y almacena la suscripción con su propietario, filtros, URL, secreto y expiración. Luego responde:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"id": "sub_123",
"refreshBefore": "2026-10-02T12:00:00Z",
"cursor": null,
"truncated": false
}
}
Tres reglas mantienen las suscripciones correctas:
- IDs Determinísticos. Deriva
iddel principal autenticado, la URL de devolución de llamada, el nombre del evento y los argumentos. El esquema de diseño sugiere un SHA-256 truncado de esa clave. - Upserts Idempotentes. Una suscripción repetida con la misma identidad actualiza el registro existente. Compara los argumentos como JSON canónico para que el orden de las claves no pueda crear duplicados.
- Actualizar antes de
refreshBefore. ChatGPT vuelve a llamar aevents/subscribeantes de ese tiempo con la misma identidad y su último cursor guardado; tú devuelves una nueva expiración. Si una actualización lleva un nuevo secreto, reemplázalo y firma con ambas claves durante un corto período. Devuelvecursor: nullpara eventos que no puedas reproducir.
Verifica la devolución de llamada antes de cualquier dato
Antes de enviar datos de la aplicación, POSTea un cuerpo de verificación firmado con un desafío nuevo, de un solo uso y de corta duración:
{ "type": "verification", "challenge": "a-single-use-random-value" }
Asígnale un webhook-id único (por ejemplo, msg_verification_123) y los mismos encabezados de firma que una entrega. ChatGPT responde con {"challenge": "a-single-use-random-value"} con un 2xx. Compara en tiempo constante, luego activa la entrega. En caso de fallo, devuelve el error JSON-RPC -32015 (CallbackEndpointError) con un data.reason como challenge_failed o timeout. Almacena en caché un pase por principal y URL durante un período limitado para que las actualizaciones no vuelvan a desafiar.
El desafío existe porque el suscriptor proporciona el secreto: sin él, cualquiera podría apuntar tu servidor a la URL de una víctima e inundarlo. Aplica el mismo endurecimiento a cada solicitud saliente: solo HTTPS, resuelve y valida la dirección en el momento de la conexión, bloquea direcciones privadas, locales y otras no públicas, y nunca sigas redireccionamientos.
Entrega y firma eventos
Cuando ocurre un evento coincidente, POSTea un objeto de evento a la URL de devolución de llamada:
{
"eventId": "evt_456",
"name": "comment.created",
"timestamp": "2026-10-01T12:05:00Z",
"data": {
"document_id": "doc_123",
"comment_id": "comment_456",
"text": "Can we add the rollout dates to this section?",
"url": "https://docs.example.com/doc_123#comment_456"
},
"cursor": null
}
Envía Content-Type: application/json, webhook-id (igual a eventId), webhook-timestamp (segundos Unix en el momento de la firma), webhook-signature, y X-MCP-Subscription-Id. Serializa el cuerpo una vez y envía esos bytes exactos, ya que la firma los cubre.
Mantente por debajo de 256 KiB; para registros grandes, envía un resumen y expón una herramienta de lectura. Trata el texto creado por el usuario como datos y nunca coloques instrucciones del modelo en una carga útil. Reintenta los fallos transitorios con un retroceso exponencial limitado, manteniendo el ID del evento y firmando cada intento de nuevo. No reintentes 410 o 413. Los eventos pueden llegar desordenados, así que haz que las herramientas de escritura sean idempotentes. La guía de diseño de webhooks fiables cubre los calendarios de reintento.
Verifica una firma de Webhooks Estándar
Según la especificación de Webhooks Estándar, el contenido firmado es ${webhook-id}.${webhook-timestamp}.${body}, la clave HMAC-SHA256 es el secreto decodificado en base64 después de eliminar whsec_, y el encabezado contiene una o más firmas v1,<base64> separadas por espacios. El borrador de MCP indica a los receptores que rechacen las marcas de tiempo de más de 5 minutos y que eliminen duplicados basándose en el webhook-id.
ChatGPT verifica por su parte, pero un receptor estricto propio sirve como un buen oráculo de prueba local. Este utiliza solo funciones nativas de Node:
// receiver.mjs: receptor estricto de Webhooks Estándar para pruebas locales (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);
Ejecútalo con WEBHOOK_SECRET=whsec_... node receiver.mjs. Devuelve 413 por encima de 256 KiB y 401 para una firma incorrecta o caducada, reproduce los desafíos y registra cada webhook-id una vez. Verificamos verify() contra el vector de prueba de firma en la biblioteca JavaScript de Webhooks Estándar. Para la teoría, lee verificación de firma de webhook.
Tu servidor bloquea las direcciones privadas, por lo que no entregará a localhost por defecto. El borrador permite objetivos no públicos solo cuando se configuran explícitamente: añade una entrada en la lista de permitidos solo para desarrollo, o expón el receptor a través de un túnel.
Pruébalo antes de que lo haga ChatGPT
Aquí tienes una pasada de prueba de Apidog para los modos de fallo que lista OpenAI. Crea un entorno con MCP_URL, MCP_TOKEN, CALLBACK_URL y WEBHOOK_SECRET, y envía cada llamada JSON-RPC como un POST a {{MCP_URL}} con Authorization: Bearer {{MCP_TOKEN}} más los encabezados que el enlace HTTP transmissible requiere: Accept: application/json, text/event-stream, MCP-Protocol-Version: 2026-07-28, y Mcp-Method establecido al método. Cada cuerpo también necesita params._meta con io.modelcontextprotocol/protocolVersion y io.modelcontextprotocol/clientCapabilities. Los ejemplos de OpenAI lo omiten, y un _meta ausente también devuelve -32602, por lo que el paso 3 pasaría por la razón equivocada.
- Descubrimiento. Envía
server/discovery verifica que$.result.capabilities.eventsexista y que$.result.supportedVersionscontenga2026-07-28. Envíaevents/listy verifica que eldeliveryde cada evento incluyawebhook. - Suscripción idempotente. Envía el cuerpo de suscripción anterior usando
{{CALLBACK_URL}}y{{WEBHOOK_SECRET}}, con un posprocesador de Extracción de Variables que guarde$.result.idcomoSUB_ID. Reenvíalo sin cambios, luego con las claves deargumentsreordenadas, y verifica que$.result.idsea igual a{{SUB_ID}}en ambas ocasiones. - Validación. Envía un secreto que decodifique a menos de 24 bytes, luego una devolución de llamada
http://. El borrador asigna ambos a-32602(InvalidParams); verifica el$.error.code. Una devolución de llamada con IP privada también debe fallar. - Desafío de verificación. Crea un endpoint simulado de Apidog que responda
{"challenge": "wrong"}y suscríbete con su URL de mock en la nube como la devolución de llamada. Verifica que$.error.codesea-32015y$.error.data.reasonseachallenge_failed. Luego, apuntaCALLBACK_URLa tu receptor y confirma que la suscripción se realiza correctamente. - Carga útil excesiva. Dispara un evento cuyo cuerpo supere los 262.144 bytes. Tu remitente debería rechazarlo; si uno se cuela, el receptor responde
413, y los registros de tu servidor deberían mostrar un solo intento, no un reintento. - Reproducción y manipulación. Copia los encabezados y el cuerpo de una entrega firmada del registro de salida de tu servidor en una nueva solicitud de Apidog. Reenvíala de inmediato:
200, y ninguna segunda línea de registro. Cambia un byte del cuerpo:401. Reenvía después de 5 minutos:401.
Guarda los pasos como un escenario y ejecútalo en CI con la CLI de Apidog. El manual de pruebas de servidores MCP cubre las llamadas a herramientas, y cómo probar webhooks profundiza en los receptores. Luego, conecta el servidor a ChatGPT a través de un plugin y ejecuta la lista de verificación del ciclo de vida de OpenAI.
Preguntas Frecuentes
¿Qué son los Eventos MCP? Una extensión experimental de MCP que permite a un servidor enviar notificaciones de eventos a un cliente en lugar de que el cliente las sondee. ChatGPT es compatible con su modo webhook en 2026-07-28.
¿ChatGPT es compatible con el sondeo o la transmisión para los Eventos MCP? No. ChatGPT solo es compatible con la entrega por webhook y la verificación de devolución de llamada. El sondeo, la transmisión y las notificaciones de gap y terminated no son compatibles.
¿Qué planes de ChatGPT incluyen los Eventos MCP? El resumen del DevDay de OpenAI dice que la característica está disponible para todos los planes.
¿Quién crea el secreto de firma? El suscriptor. ChatGPT envía un secreto whsec_ en delivery.secret; tu servidor lo valida, almacena y firma con él, y nunca genera el suyo propio.
¿En qué se diferencia esto de la API de Agentes? Los Eventos MCP envían datos desde tu servidor a ChatGPT. La API de Agentes de OpenAI ejecuta los agentes que construyes e informa su progreso mediante transmisión o webhooks.
Siguiente paso
Añade "events": {} a un servidor, expón un evento con un filtro y ejecuta las seis comprobaciones contra un receptor local antes de conectar ChatGPT. Descarga Apidog para mantener esas comprobaciones como un escenario que se ejecuta en cada commit.
