Eventos MCP permitem que seu servidor MCP envie atualizações para o ChatGPT no momento em que elas acontecem, para que nenhum agente precise consultá-las (polling). Desde o OpenAI DevDay em 29 de setembro de 2026, o ChatGPT suporta a especificação proposta de Eventos MCP na versão de protocolo 2026-07-28 (MCP 2.0), em todos os planos. Seu servidor adiciona três métodos (events/list, events/subscribe, events/unsubscribe), anuncia events em server/discover, passa por um desafio de verificação de callback e assina cada entrega com HMAC de Webhooks Padrão. O ChatGPT aceita apenas entrega via webhook.
Este guia aborda cada formato de mensagem documentado pela OpenAI, as regras de segurança que seu servidor aplica e um teste completo para todo o ciclo. Novo no protocolo? Comece com o que é MCP. Você enviará as chamadas JSON-RPC e simulará o callback no Apidog.
Eventos MCP em um piscar de olhos
| Item | O que o ChatGPT espera |
|---|---|
| Protocolo | MCP 2.0, versão 2026-07-28 |
| Capacidade | "events": {} nas capacidades de server/discover |
| Métodos | events/list, events/subscribe, events/unsubscribe, no mesmo endpoint autenticado das suas ferramentas |
| Entrega | Apenas webhook: sem polling, streaming, notificações de gap ou terminated |
| Assinatura | HMAC-SHA256 de Webhooks Padrão |
| Cabeçalhos | webhook-id, webhook-timestamp, webhook-signature, X-MCP-Subscription-Id |
| Segredo | whsec_ mais base64 que decodifica para 24-64 bytes, fornecido pelo ChatGPT |
| Limite de Payload | 256 KiB (262.144 bytes), um evento por requisição |
| ID da Assinatura | Determinístico: principal, URL de callback, nome do evento, argumentos |
| Callbacks | HTTPS, verificado por desafio, sem endereços privados, sem redirecionamentos |
Fontes: guia de Eventos MCP da OpenAI e o rascunho de esboço de design dos Eventos MCP.
Por que eventos em vez de polling
Sem eventos, um agente monitorando comentários de revisão chama uma ferramenta em um temporizador e compara os resultados, consumindo requisições quando nada muda e atrasando quando algo acontece. Eventos MCP invertem a direção: seu servidor sabe quando um registro muda, então ele envia a atualização. As compensações são as clássicas de webhooks vs polling, agora dentro do MCP.
O resumo do DevDay da OpenAI usa um quadro de projeto: o usuário pede ao ChatGPT para monitorar novas tarefas, e quando uma aparece, o ChatGPT lê os documentos vinculados e elabora um plano. Os documentos adicionam mais dois casos: transformar relatórios de bugs em um canal em rascunhos de pull requests (message.created, filtrado por channel_id), e aplicar comentários de revisão a um documento (comment.created, filtrado por document_id).
A especificação vem do Grupo de Trabalho de Triggers e Eventos do MCP (repositório) e é rotulada como experimental, então fixe seu código em 2026-07-28. Para o restante do lançamento, consulte o hub do DevDay 2026.
Anuncie e defina eventos
Adicione events às capacidades na sua resposta de server/discover:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": { "tools": {}, "events": {} }
}
}
Em seguida, retorne as definições de events/list. Cada uma contém um name, uma description, delivery: ["webhook"], um inputSchema para argumentos de assinatura (seus filtros, como document_id), e um payloadSchema para o objeto data em cada entrega. Use nomes estáveis e descrições específicas, aplique filtros em seu servidor e liste apenas os eventos que a conta conectada pode ver.
events/subscribe: requisição e resposta
Quando um usuário pede ao ChatGPT para monitorar algo, o ChatGPT chama 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 aceitar, autorize o usuário para esse evento e esses argumentos, valide ambos contra sua definição, exija um segredo whsec_ cujo base64 decodifique para 24-64 bytes, verifique o callback (próxima seção) e armazene a assinatura com seu proprietário, filtros, URL, segredo e expiração. Em seguida, responda:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"id": "sub_123",
"refreshBefore": "2026-10-02T12:00:00Z",
"cursor": null,
"truncated": false
}
}
Três regras mantêm as assinaturas corretas:
- IDs Determinísticos. Derive
iddo principal autenticado, URL de callback, nome do evento e argumentos. O esboço de design sugere um SHA-256 truncado dessa chave. - Upserts Idempotentes. Uma assinatura repetida com a mesma identidade atualiza o registro existente. Compare os argumentos como JSON canônico para que a ordem das chaves não crie duplicatas.
- Atualizar antes de
refreshBefore. O ChatGPT chamaevents/subscribenovamente antes desse tempo com a mesma identidade e seu último cursor salvo; você retorna uma nova expiração. Se uma atualização contiver um novo segredo, substitua-o e assine com ambas as chaves por um curto período. Retornecursor: nullpara eventos que você não pode reproduzir.
Verifique o callback antes de qualquer dado
Antes de enviar dados da aplicação, faça um POST de um corpo de verificação assinado com um desafio novo, de uso único e de curta duração:
{ "type": "verification", "challenge": "a-single-use-random-value" }
Atribua a ele um webhook-id único (por exemplo msg_verification_123) e os mesmos cabeçalhos de assinatura de uma entrega. O ChatGPT ecoa {"challenge": "a-single-use-random-value"} com um 2xx. Compare em tempo constante e, em seguida, ative a entrega. Em caso de falha, retorne o erro JSON-RPC -32015 (CallbackEndpointError) com um data.reason como challenge_failed ou timeout. Armazene em cache uma aprovação por principal e URL por um período limitado para que as atualizações não exijam um novo desafio.
O desafio existe porque o assinante fornece o segredo: sem ele, qualquer um poderia apontar seu servidor para a URL de uma vítima e inundá-la. Aplique o mesmo endurecimento a cada requisição de saída: apenas HTTPS, resolva e valide o endereço no momento da conexão, bloqueie endereços privados, locais e outros não públicos, e nunca siga redirecionamentos.
Entregue e assine eventos
Quando um evento correspondente ocorre, faça um POST de um objeto de evento para a URL de callback:
{
"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
}
Envie Content-Type: application/json, webhook-id (igual a eventId), webhook-timestamp (segundos Unix no momento da assinatura), webhook-signature e X-MCP-Subscription-Id. Serialize o corpo uma vez e envie esses bytes exatos, já que a assinatura os cobre.
Mantenha-se abaixo de 256 KiB; para registros grandes, envie um resumo e exponha uma ferramenta de leitura. Trate o texto de autoria do usuário como dados e nunca coloque instruções do modelo em um payload. Tente novamente falhas transitórias com backoff exponencial limitado, mantendo o ID do evento e assinando cada tentativa novamente. Não tente novamente 410 ou 413. Os eventos podem chegar fora de ordem, então torne as ferramentas de escrita idempotentes. O guia de design de webhooks confiáveis cobre os agendamentos de retentativa.
Verifique uma assinatura de Webhooks Padrão
De acordo com a especificação de Webhooks Padrão, o conteúdo assinado é ${webhook-id}.${webhook-timestamp}.${body}, a chave HMAC-SHA256 é o segredo decodificado em base64 após remover whsec_, e o cabeçalho contém uma ou mais assinaturas v1,<base64> separadas por espaço. O rascunho do MCP instrui os receptores a rejeitar timestamps com mais de 5 minutos e a deduplicar com base no webhook-id.
O ChatGPT verifica do seu lado, mas um receptor rigoroso seu serve como um bom oráculo de teste local. Este usa apenas recursos nativos do Node:
// receiver.mjs: strict Standard Webhooks receiver for local tests (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);
Execute-o com WEBHOOK_SECRET=whsec_... node receiver.mjs. Ele retorna 413 acima de 256 KiB e 401 para uma assinatura inválida ou expirada, ecoa desafios e registra cada webhook-id uma vez. Verificamos verify() contra o vetor de teste de assinatura na biblioteca JavaScript de Webhooks Padrão. Para a teoria, leia verificação de assinatura de webhook.
Seu servidor bloqueia endereços privados, então ele não entregará para localhost por padrão. O rascunho permite alvos não públicos apenas quando explicitamente configurados: adicione uma entrada na lista de permissões apenas para desenvolvimento, ou exponha o receptor através de um túnel.
Teste antes que o ChatGPT o faça
Aqui está um teste do Apidog para os modos de falha listados pela OpenAI. Crie um ambiente com MCP_URL, MCP_TOKEN, CALLBACK_URL e WEBHOOK_SECRET, e envie cada chamada JSON-RPC como um POST para {{MCP_URL}} com Authorization: Bearer {{MCP_TOKEN}} mais os cabeçalhos que o Streamable HTTP binding exige: Accept: application/json, text/event-stream, MCP-Protocol-Version: 2026-07-28, e Mcp-Method definido para o método. Cada corpo também precisa de params._meta com io.modelcontextprotocol/protocolVersion e io.modelcontextprotocol/clientCapabilities. Os exemplos da OpenAI o omitem, e um _meta ausente também retorna -32602, então a etapa 3 passaria pelo motivo errado.
- Descoberta. Envie
server/discovere afirme que$.result.capabilities.eventsexiste e$.result.supportedVersionscontém2026-07-28. Envieevents/liste afirme que odeliveryde cada evento incluiwebhook. - Assinatura idempotente. Envie o corpo de assinatura acima usando
{{CALLBACK_URL}}e{{WEBHOOK_SECRET}}, com um pós-processador de Extração de Variável que salva$.result.idcomoSUB_ID. Reenvie-o inalterado, depois com as chavesargumentsreordenadas, e afirme que$.result.idé igual a{{SUB_ID}}em ambas as vezes. - Validação. Envie um segredo que decodifica para menos de 24 bytes, depois um callback
http://. O rascunho mapeia ambos para-32602(InvalidParams); afirme sobre$.error.code. Um callback de IP privado também deve falhar. - Desafio de verificação. Crie um endpoint de mock do Apidog que responda
{"challenge": "wrong"}e assine com sua URL de mock na nuvem como callback. Afirme que$.error.codeé-32015e$.error.data.reasonéchallenge_failed. Em seguida, aponteCALLBACK_URLpara o seu receptor e confirme que a assinatura é bem-sucedida. - Payload excessivo. Dispare um evento cujo corpo exceda 262.144 bytes. Seu remetente deve recusá-lo; se um passar, o receptor responde
413, e os logs do seu servidor devem mostrar uma tentativa, não uma retentativa. - Reprodução e adulteração. Copie os cabeçalhos e o corpo de uma entrega assinada do log de saída do seu servidor para uma nova requisição Apidog. Reenvie-a imediatamente:
200, e nenhuma segunda linha de log. Altere um byte do corpo:401. Reenvie após 5 minutos:401.
Salve os passos como um cenário e execute-o em CI com o Apidog CLI. O manual de testes de servidor MCP cobre chamadas de ferramentas, e como testar webhooks aprofunda-se nos receptores. Em seguida, conecte o servidor ao ChatGPT através de um plugin e execute a lista de verificação de ciclo de vida da OpenAI.
Perguntas Frequentes (FAQ)
O que são Eventos MCP? Uma extensão experimental do MCP que permite a um servidor enviar notificações de eventos para um cliente em vez de o cliente fazer polling. O ChatGPT suporta seu modo de webhook em 2026-07-28.
O ChatGPT suporta polling ou streaming para Eventos MCP? Não. O ChatGPT suporta apenas entrega via webhook e verificação de callback. Polling, streaming e as notificações de gap e terminated não são suportados.
Quais planos do ChatGPT recebem Eventos MCP? O resumo do DevDay da OpenAI afirma que o recurso está disponível para todos os planos.
Quem cria o segredo de assinatura? O assinante. O ChatGPT envia um segredo whsec_ em delivery.secret; seu servidor o valida, armazena e assina com ele, e nunca gera o seu próprio.
Como isso difere da API de Agentes? Eventos MCP enviam dados do seu servidor para o ChatGPT. A API de Agentes da OpenAI executa agentes que você constrói e relata seu progresso por streaming ou webhooks.
Próximo passo
Adicione "events": {} a um servidor, exponha um evento com um filtro e execute as seis verificações contra um receptor local antes de conectar o ChatGPT. Baixe o Apidog para manter essas verificações como um cenário que é executado a cada commit.
