1. Para que servem os eventos MCP
Até agora, quase toda a comunicação entre o ChatGPT e o seu backend parecia um RPC: o modelo chamava uma ferramenta, ela fazia algo, retornava o resultado — pronto. Isso é conveniente enquanto as operações são curtas: 200–500 ms, no máximo alguns segundos.
Mas assim que surge algo de longa duração — análise de um arquivo grande com preferências de funcionários para o GiftGenius, agregação de recomendações de um monte de APIs externas, recálculo de um feed volumoso — tudo fica desagradável. Timeouts de HTTP, reinicializações de funções, spinners “eternos”, e o usuário fica se perguntando: “isso ainda está vivo ou já morreu?”.
É aqui que entra o modelo de eventos. Em vez de manter uma única chamada longa da ferramenta, você inicia uma tarefa, recebe um jobId e, a partir daí, o servidor, por iniciativa própria, envia eventos: começou, progresso em andamento, concluído, falhou. Esses eventos no MCP são implementados como JSON-RPC notifications — mensagens unidirecionais sem id, para as quais não se espera resposta.
É importante entender: um evento não é um “console.log no fio”. É uma mensagem formal de protocolo com um esquema definido, que seu UI (widget) e/ou agente deve ser capaz de processar com a mesma disciplina que o resultado da chamada de uma ferramenta.
Lembrete: tipos de mensagens no MCP
Antes de avançar, vamos relembrar rapidamente que tipos de mensagens existem no MCP.
Sem os vernizes de marketing, o MCP se baseia em JSON-RPC 2.0. Lá existem três tipos básicos de mensagens: requisições, respostas e notificações.
Em vez de listá-los, vejamos uma pequena tabela comparativa:
| Tipo | Campo id | Quem inicia | Resposta esperada? | Exemplo no MCP |
|---|---|---|---|---|
| Request | sim | Normalmente o cliente (ChatGPT) | Sim | Chamada de ferramenta tools/call |
| Response | sim | Servidor MCP | É a própria resposta | Resultado de tools/call |
| Notification | não | Cliente ou servidor | Não | notifications/progress, resources/updated, logging/message |
Os eventos MCP vivem exatamente na terceira linha: são notifications. Sinais distintivos:
- não há id no nível superior — nenhum result ou error virá em resposta;
- o iniciador não espera ACK — “fire-and-forget” no nível do protocolo;
- a confiabilidade não é construída com confirmações, mas com a idempotência dos manipuladores e a política de reenvios.
Importante: os eventos MCP não “voam por aí a qualquer momento no espaço”. Eles vivem dentro de uma conexão MCP estabelecida sobre um transporte concreto. Na maioria das vezes é um fluxo como SSE (os detalhes de transporte e suas variantes veremos em uma aula separada).
2. O que é um “evento MCP” na prática
Formalmente, um evento MCP é uma JSON-RPC notification, isto é, um objeto do tipo:
{
"jsonrpc": "2.0",
"method": "notifications/job/progress",
"params": {
"jobId": "job_123",
"percentage": 30,
"stage": "Procurando opções no catálogo",
"eventId": "evt_abc123",
"timestamp": "2025-11-21T10:15:00Z"
}
}
Aqui há alguns pontos importantes:
- No campo method codificamos o tipo do evento e seu “namespace”. O MCP já define uma série de métodos padrão do tipo notifications/... para logs, progresso e alteração de recursos, mas você pode e deve adicionar seus métodos específicos de negócio, como notifications/job/progress ou notifications/job/completed.
- Todos os dados de negócio ficam em params. Lá também manteremos os identificadores das tarefas (jobId), ids únicos de eventos (eventId), horário (timestamp), mensagens legíveis por humanos e outros.
- O campo id está ausente no nível superior — é por isso que é uma notification. O protocolo não prevê uma resposta para ela. Se o servidor quiser saber “se foi compreendido”, pode enviar outro evento ou aguardar ações reativas do cliente (por exemplo, uma nova requisição). Mas não há ACK em termos de JSON-RPC.
No nível do modelo mental, pense assim: a chamada de ferramenta tools/call é “uma carta pela qual você espera resposta”, e o evento é “uma notificação de um bot do Slack: “Tarefa em segundo plano #123 concluída””.
3. Taxonomia de eventos: que notificações existem
Se simplesmente permitirmos “enviem quaisquer JSONs como notifications”, em duas semanas o sistema vira uma bagunça: nomes de eventos diferentes, campos variando, o UI sem saber o que fazer com aquilo. Por isso, é útil concordar uma pequena taxonomia.
Abaixo está uma classificação conveniente que se encaixa bem com a especificação do MCP e com casos reais de ChatGPT Apps.
Eventos do ciclo de vida da tarefa (Job Lifecycle)
São eventos que refletem transições de estado chave da tarefa. Normalmente, a tarefa tem uma máquina de estados (state machine) como pending → running → (completed | failed | canceled).
Eventos típicos:
- job.created — tarefa registrada;
- job.started — o worker começou a executar o trabalho;
- job.completed — tarefa concluída com sucesso;
- job.failed — a tarefa falhou com erro;
- job.canceled — a tarefa foi cancelada pelo usuário.
Exemplo de job.completed para o GiftGenius:
{
"jsonrpc": "2.0",
"method": "notifications/job/completed",
"params": {
"eventId": "evt_gg_100",
"jobId": "giftjob_42",
"timestamp": "2025-11-21T10:20:00Z",
"summary": "Seleção de presentes concluída",
"resultResourceId": "resource:gifts:giftjob_42"
}
}
Aqui, resultResourceId pode apontar para um recurso do MCP que será lido depois pelo widget ou pelo agente.
Eventos de progresso (Progress Updates)
São “passos pequenos” dentro do ciclo de vida: eles não mudam o status final, mas dão ao usuário a sensação de que algo está acontecendo.
Evento típico job.progress:
{
"jsonrpc": "2.0",
"method": "notifications/job/progress",
"params": {
"eventId": "evt_gg_101",
"jobId": "giftjob_42",
"timestamp": "2025-11-21T10:18:30Z",
"percentage": 40,
"stage": "Filtrando presentes por orçamento",
"etaSeconds": 25
}
}
Aqui é importante que o percentage avance de forma razoável em direção a 100, sem ficar indo e voltando. Escolha um único nome para o campo de progresso (por exemplo, percentage) e use-o em todos os eventos. Na utilidade oficial de progresso do MCP, também existe a regra: o progresso só cresce.
Eventos de atualização de dados (Resource/Data events)
Às vezes o usuário nem se importa com um jobId específico. O que importa é que alguma entidade mudou: o feed de produtos foi atualizado, um novo snapshot de relatório foi gerado, um perfil pessoal foi reprocessado.
No MCP já existem notificações padrão de nível de recurso como resources/updated, resources/list_changed e semelhantes, que sinalizam ao cliente: “releia a lista de recursos, algo mudou lá”.
Para o GiftGenius, isso pode ser assim:
{
"jsonrpc": "2.0",
"method": "resources/updated",
"params": {
"eventId": "evt_feed_17",
"timestamp": "2025-11-21T09:00:00Z",
"resourceId": "resource:product-feed",
"changeType": "snapshot_ready"
}
}
O widget, ao receber tal evento, pode, por exemplo, destacar o botão “Atualizar lista de presentes”.
Eventos de UX e do sistema
Há também eventos que não são estritamente de negócio, mas são importantes para UX ou diagnóstico:
- mensagens de log logging/message — notificação padrão do MCP para logs;
- heartbeat/ping — “estou vivo” periódicos do servidor;
- alertas de degradação: por exemplo, “a API externa está lenta agora, os resultados podem chegar mais devagar”.
Esses eventos são úteis para monitoramento e depuração; às vezes podem ser aproveitados no UI, mostrando à pessoa que o sistema não morreu, apenas está ocupado.
4. Estrutura do evento: campos obrigatórios e payload
Um evento é um objeto de API como uma chamada de ferramenta. É preciso projetá-lo. Um bom hábito é acordar um conjunto básico de campos.
Conceitualmente, é útil dividir o evento em três partes: metadados, correlação e carga útil.
Exemplo de forma geral:
{
"jsonrpc": "2.0",
"method": "notifications/job/progress",
"params": {
"eventId": "evt_gg_103",
"type": "job.progress",
"timestamp": "2025-11-21T10:19:00Z",
"jobId": "giftjob_42",
"payload": {
"percentage": 60,
"stage": "Comparando avaliações",
"etaSeconds": 15
}
}
}
Nessa estrutura podemos destacar:
- eventId — identificador único do evento. Necessário para deduplicação no cliente;
- type — nome lógico do evento (pode duplicar/normalizar method);
- timestamp — quando o evento foi gerado pelo servidor;
- jobId ou outro correlation-id — para entender a que o evento se refere;
- payload — os dados em si. Para cada tipo de evento, tem sua própria forma.
Em um sistema real, você quase certamente vai querer descrever formalmente essas estruturas com JSON Schema ou pelo menos tipos TypeScript, para que servidor e cliente validem as mensagens. Em algumas equipes, usa-se um formato inspirado em CloudEvents: lá também há campos padrão como id, source, type, time etc.
Mas a ideia-chave é simples: o evento deve ser legível por máquina e consistente — sem surpresas do tipo “às vezes o campo se chama jobId, às vezes job_id, às vezes não existe”.
Nos exemplos a seguir, para não sobrecarregar o código, vamos usar com mais frequência a variante “achatada”: todos os dados do evento ficam diretamente em params sem o payload aninhado, e o campo type às vezes é omitido, se seu papel já for desempenhado por method. O princípio permanece o mesmo: cada evento tem metadados estáveis (eventId, jobId, timestamp) e uma carga útil previsível.
5. Idempotência de eventos: por quê e como
Agora a palavra mais importante desta aula — idempotência.
Idempotência do manipulador de evento significa que, se o mesmo evento for processado uma vez ou dez vezes, o estado final do sistema permanecerá correto. Em sistemas distribuídos com rede e retries, isso é literalmente questão de vida ou morte.
Por que o mesmo evento pode chegar várias vezes?
Vários motivos: de quedas de conexão e reconexões a retries no lado do servidor, que “por via das dúvidas” enviou a notificação novamente. Ao usar protocolos de fluxo (por exemplo, quando o servidor empurra eventos em uma conexão aberta, como SSE — mais sobre isso em uma aula separada sobre transporte), isso é clássico: o cliente reconectou com Last-Event-ID, o servidor reenvia eventos perdidos e alguns deles o cliente verá pela segunda vez.
Se o seu manipulador não for idempotente, as estranhezas começam:
- o evento job.completed gera cobrança em dobro de bônus ou altera o status do pedido duas vezes;
- o evento resource.updated faz o widget “adicionar” cartões a cada vez, duplicando-os no UI;
- job.progress repetidos assustam os usuários se a barra de progresso começa a ir para frente e para trás.
A estratégia correta funciona em duas camadas: geração de eventos no servidor e seu processamento no cliente.
Lado do servidor: ids estáveis e máquina de estados
O servidor deve:
- gerar um eventId único para cada evento lógico;
- garantir que os eventos de um mesmo jobId formem uma sequência de estados válida: você não pode enviar job.failed depois de job.completed ou dois job.completed diferentes com resultados diferentes.
Ou seja, você tem de fato uma máquina de estados da tarefa, e cada evento é uma transição permitida.
Lado do cliente: deduplicação e atualizações “suaves”
O cliente (widget, agente ou outro componente) deve:
- armazenar o conjunto de eventId já processados pelo menos durante a vida da conexão/sessão atual;
- verificar antes de processar: se o eventId já foi visto, simplesmente ignorar ou redesenhar o UI sem efeitos colaterais;
- ao receber eventos que mudam o status da tarefa (job.completed, job.failed), garantir que a transição é válida: por exemplo, se a tarefa já está marcada como completed, um job.completed repetido não deve mudar nada, e um failed é melhor ignorar como incorreto.
Exemplo clássico do mundo de commerce: processamento do webhook de confirmação de pagamento. Um mesmo order.paid pode chegar duas vezes; portanto o backend guarda paymentId e um flag “já creditado”. Mesmo que o webhook chegue de novo, o estado do pedido não muda. Os eventos MCP devem ser projetados com a mesma mentalidade.
6. Exemplo: projetando eventos para o GiftGenius
Vamos levar isso para o nosso GiftGenius de treino. Imagine um cenário longo: o usuário carregou um CSV grande com a lista de funcionários e seus interesses e pediu “escolher ideias de presente para todos”. A operação pode levar dezenas de segundos.
Um modelo razoável de eventos pode ser descrito assim:
- O usuário executa a ferramenta start_bulk_gift_analysis. A ferramenta retorna um jobId: "bulk_2025_001".
- O servidor MCP cria a tarefa e quase imediatamente envia job.started com uma breve descrição.
- À medida que executa, ele envia vários job.progress com etapas:
- 10% — “Fazendo parse do arquivo e verificando o formato”;
- 40% — “Extraindo interesses e departamentos”;
- 70% — “Relacionando presentes por categoria”;
- 100% — pouco antes da conclusão.
- No fim chega job.completed com um link para o recurso com as recomendações finais.
- Se tudo der errado — em vez de completed chega job.failed com um código de erro e, possivelmente, uma dica do que corrigir.
Informalmente, é isso mesmo, mas vamos fixar isso como JSON Schemas para dois eventos-chave job.progress e job.completed. Pseudo-JSON Schema (simplificada):
{
"job.progress": {
"type": "object",
"properties": {
"eventId": { "type": "string" },
"jobId": { "type": "string" },
"timestamp": { "type": "string", "format": "date-time" },
"percentage": { "type": "number", "minimum": 0, "maximum": 100 },
"stage": { "type": "string" },
"etaSeconds": { "type": "number" }
},
"required": ["eventId", "jobId", "timestamp", "percentage", "stage"]
}
}
{
"job.completed": {
"type": "object",
"properties": {
"eventId": { "type": "string" },
"jobId": { "type": "string" },
"timestamp": { "type": "string", "format": "date-time" },
"summary": { "type": "string" },
"resultResourceId": { "type": "string" }
},
"required": ["eventId", "jobId", "timestamp", "resultResourceId"]
}
}
Você não é obrigado a implementar agora uma validação completa de schemas, mas manter essa estrutura em mente é útil: ajuda a não “espalhar” campos em formatos diferentes e a não esquecer metadados importantes.
7. Mini-prática: servidor que envia eventos MCP
Vamos agora juntar a teoria a um pequeno trecho de pseudo-código TypeScript. Não vamos entrar em bibliotecas reais do MCP (primeiro, porque ainda estão evoluindo; segundo, porque o foco aqui é o modelo), mas vamos desenhar um esqueleto estrutural.
Suponha que nosso servidor MCP tenha a abstração sendNotification, que sabe enviar uma JSON-RPC notification de volta ao ChatGPT. Pseudo-interface:
// Utilitário para enviar uma MCP notification
async function sendNotification(
method: string,
params: Record<string, unknown>
) {
// Aqui você serializaria o JSON e enviaria pela conexão MCP ativa
}
Agora implementamos o manipulador da ferramenta start_bulk_gift_analysis. Ele registra a tarefa, retorna o jobId e, em algum lugar em background, “anda” e envia progresso. Na vida real, isso seria um worker e uma fila, mas por ora vamos de timer.
type Job = {
id: string;
status: "pending" | "running" | "completed" | "failed";
};
const jobs = new Map<string, Job>();
export async function startBulkGiftAnalysisTool() {
const jobId = `bulk_${Date.now()}`;
jobs.set(jobId, { id: jobId, status: "pending" });
// Enviamos job.started imediatamente
await sendNotification("notifications/job/started", {
eventId: `evt_${jobId}_started`,
jobId,
timestamp: new Date().toISOString(),
summary: "Análise de lista grande de presentes iniciada"
});
simulateJob(jobId); // "iniciamos" a tarefa em background
return { jobId };
}
Simulação da tarefa:
async function simulateJob(jobId: string) {
jobs.set(jobId, { id: jobId, status: "running" });
const stages = [
{ percent: 10, stage: "Analisando CSV" },
{ percent: 40, stage: "Analisando interesses" },
{ percent: 70, stage: "Selecionando presentes" },
{ percent: 100, stage: "Gerando resultado" }
];
for (const s of stages) {
await sendNotification("notifications/job/progress", {
eventId: `evt_${jobId}_${s.percent}`,
jobId,
timestamp: new Date().toISOString(),
percentage: s.percent,
stage: s.stage
});
await new Promise(r => setTimeout(r, 1000));
}
jobs.set(jobId, { id: jobId, status: "completed" });
await sendNotification("notifications/job/completed", {
eventId: `evt_${jobId}_done`,
jobId,
timestamp: new Date().toISOString(),
summary: "Análise de presentes concluída",
resultResourceId: `resource:gifts:${jobId}`
});
}
O código é propositalmente simples, mas mostra bem:
- usamos a sequência de eventos started → progress* → completed;
- cada evento recebe um eventId único;
- todos os eventos estão vinculados a um mesmo jobId.
No futuro, quando você adicionar filas e workers reais, a estrutura dos eventos permanecerá aproximadamente a mesma — só mudará o local onde sendNotification é chamada.
8. Cliente: o manipulador de eventos idempotente mais simples
No lado do cliente (por exemplo, no seu widget do Apps SDK), é preciso aprender a receber tais eventos, vinculá-los às tarefas atuais e não enlouquecer com duplicados.
Sem entrar no transporte por agora (disso falaremos depois), imagine uma função onMcpNotification, que sua camada de cliente MCP chama a cada notification recebida.
Vamos adicionar uma deduplicação simples:
const processedEvents = new Set<string>();
function handleNotification(method: string, params: any) {
const eventId = params.eventId as string | undefined;
if (!eventId) return; // muito discutível, mas serve para o exemplo
if (processedEvents.has(eventId)) {
// Duplicado — ignoramos ou atualizamos o UI de forma suave
return;
}
processedEvents.add(eventId);
if (method === "notifications/job/progress") {
updateJobProgress(params.jobId, params.percentage, params.stage);
} else if (method === "notifications/job/completed") {
markJobCompleted(params.jobId, params.resultResourceId);
}
}
A implementação de updateJobProgress e markJobCompleted já é puro código de React/UI:
function updateJobProgress(jobId: string, percent: number, stage: string) {
// por exemplo, colocamos em Zustand/Redux/React state
console.log(`Job ${jobId}: ${percent}% — ${stage}`);
}
function markJobCompleted(jobId: string, resourceId: string) {
console.log(`Job ${jobId} concluído, recurso: ${resourceId}`);
}
Esse manipulador:
- não quebra se o evento chegar duas vezes;
- não causa efeitos colaterais (tipo “mostrar de novo o modal ‘Concluído!’”);
- abre caminho para lógica mais complexa, por exemplo, validação de transições de estado permitidas (não permitir failed sobre um completed já existente).
Em código de produção, você provavelmente vai querer zerar processedEvents ao reconectar ao servidor MCP, além de armazenar não só o eventId, mas também o status atual de cada jobId, para se comportar de forma mais sensata diante de sequências estranhas de eventos.
Em seguida, é importante entender como todos esses eventos MCP passam pelo agente/widget e viram uma experiência concreta para o usuário: barra de progresso, etapas de execução, aparecimento dos resultados finais. Vamos ligar eventos a run/workflow e UX.
9. Ligação de eventos, run/workflow e UX
Embora já tenhamos tido um módulo completo sobre workflow e agentes, agora você verá o quadro todo. Já apresentamos famílias de eventos (job.*, resource.*, sistema); vejamos como eles passam pelo agente/widget e pelo ChatGPT e se tornam uma experiência concreta para o usuário.
O cenário típico com uma tarefa longa é assim: o ChatGPT chama a MCP-tool, obtendo um jobId; em seguida, para esse jobId, o servidor envia eventos de progresso, conclusão ou erro; seu widget ou a lógica do agente, com base neles, atualiza o UI e toma decisões.
No diagrama de sequência isso pode ser desenhado assim:
sequenceDiagram
participant User as Usuário
participant GPT as ChatGPT (modelo)
participant App as Servidor MCP do GiftGenius
participant Widget as Widget GiftGenius
User->>GPT: "Escolha presentes para 2000 funcionários"
GPT->>App: tools.call start_bulk_gift_analysis
App-->>GPT: response { jobId: "bulk_2025_001" }
GPT->>Widget: ToolOutput { jobId }
Widget->>Widget: Mostrar barra de progresso
App-->>GPT: notification job.started
App-->>GPT: notification job.progress (10%, 40%, 70%, 100%)
App-->>GPT: notification job.completed { resultResourceId }
GPT->>Widget: Encaminha eventos/dados para o widget
Widget->>User: Atualiza o progresso e mostra o resultado
Na prática, o diagrama real será um pouco mais complexo, mas a ideia principal é simples: eventos MCP são o “sistema nervoso” entre suas operações em background e a experiência do usuário.
10. Erros comuns ao trabalhar com eventos MCP
Erro nº 1: “Evento = log em formato de produção”.
Às vezes os desenvolvedores começam apenas encaminhando ao MCP o que antes escreviam em console.log. O resultado: nos eventos não há eventId, nem jobId, nem um timestamp decente, apenas mensagens semi-poéticas “estamos quase acabando”. Essa abordagem torna o sistema frágil: é difícil fazer parse, impossível deduplicar, o UI não sabe a que tarefa a mensagem pertence. É melhor desde o início projetar eventos como um contrato formal: nome de método claro, conjunto estável de campos, payload lógico.
Erro nº 2: Falta de idempotência e de eventId único.
Muitos começam com a ideia ingênua: “ora, os eventos chegam uma vez só”. Em uma semana começa: ao reconectar o cliente, as notificações duplicam, o usuário recebe a mesma coisa duas vezes, o backend comercial credita bônus em duplicidade. Sem um eventId único e uma deduplicação elementar no cliente, cedo ou tarde você terá um bug sério. Em um sistema distribuído, é preciso partir do modelo “at-least-once delivery”: duplicados são inevitáveis.
Erro nº 3: Misturar eventos de sistema e de negócio no mesmo “caldo”.
Por exemplo, no mesmo fluxo caem logging/message, job.progress, job.completed, resources/updated, tudo isso sem separação clara por type/method. Como resultado, a camada de UI começa a fazer coisas estranhas como if (message.includes("pronto")) para entender que a tarefa terminou. É melhor separar claramente: há notificações de sistema (logs, heartbeat) e há eventos de negócio (job.*, resource.*) com schemas estritamente descritos.
Erro nº 4: Transições de estado da tarefa inconsistentes.
Acontece de o servidor, em um mesmo fluxo de eventos, enviar primeiro job.completed, depois job.progress, depois job.failed. Isso ocorre se não houver uma máquina de estados explícita e verificações ao emitir eventos. Torna-se impossível para os clientes entenderem o que realmente está acontecendo. O correto é descrever um autômato finito de estados e não emitir eventos que o violem: por exemplo, após completed você pode, no máximo, enviar um evento informativo adicional, mas não voltar a tarefa para running.
Erro nº 5: Vinculação rígida aos nomes de métodos MCP da versão atual da especificação.
A especificação do MCP ainda está evoluindo. Se você amarrar tudo a métodos atuais com nomes de sistema sem considerar seus próprios namespaces, qualquer mudança no protocolo vai obrigar a reescrever metade do sistema. É melhor encarar os eventos como sua mini-especificação sobre o MCP: você pode se basear nos métodos existentes (notifications/progress, resources/updated), mas projetar os eventos de negócio (notifications/job/*) no seu próprio namespace e mantê-los relativamente independentes.
Erro nº 6: Sem ligação dos eventos com o UX.
Às vezes a equipe faz um belo modelo de eventos no backend, mas não o leva até o widget: job.progress existe nos logs, mas o UI mostra um spinner solitário por 40 segundos. Nessa situação, o usuário não confia nem no MCP, nem na IA. Ao projetar eventos, pense sempre no efeito específico de UI que você deseja obter: barra de progresso, etapas, resultados parciais. Os eventos MCP não existem por causa do protocolo, mas para gerar um comportamento claro da aplicação.
GO TO FULL VERSION