Eventos MCP Explicados: Crie e Teste um Servidor com Webhooks para ChatGPT

Eventos MCP explicados: como o ChatGPT se inscreve no seu servidor MCP com events/subscribe, assinatura de Webhooks Padrão, o limite de 256 KiB e testes no Apidog.

Medy Evrard

2 outubro 2026

Eventos MCP Explicados: Crie e Teste um Servidor com Webhooks para ChatGPT

Apidog para empresas

Implantação local

SSO & RBAC

Conforme SOC 2

Explorar Apidog Enterprise

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.

button

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:

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.

  1. Descoberta. Envie server/discover e afirme que $.result.capabilities.events existe e $.result.supportedVersions contém 2026-07-28. Envie events/list e afirme que o delivery de cada evento inclui webhook.
  2. 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.id como SUB_ID. Reenvie-o inalterado, depois com as chaves arguments reordenadas, e afirme que $.result.id é igual a {{SUB_ID}} em ambas as vezes.
  3. 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.
  4. 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 é -32015 e $.error.data.reason é challenge_failed. Em seguida, aponte CALLBACK_URL para o seu receptor e confirme que a assinatura é bem-sucedida.
  5. 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.
  6. 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.

Pratique o design de API no Apidog

Descubra uma forma mais fácil de construir e usar APIs