MCP para agentes: push, replay e compatibilidade

Use notifications para baixa latência, o stream com cursor para recuperar lacunas e um Endpoint quando você precisa acordar um worker com garantia durável.

As tools do MCP formam o plano de comando. O resource botozap://events{?after,limit} forma um canal de Eventos para clientes que mantêm uma sessão e negociam resources.subscribe. A notification é apenas um sinal para releitura: ela não inicia, por si só, uma nova execução do agente.

Meta → BotoZap → account_events (PostgreSQL)
                       ├─ notification MCP (sinal)
                       │       └─ read resource → Evento → cursor salvo
                       └─ outbox → Endpoint durável
                                  └─ fila do dev → novo agent turn

Estado da release

O pacote publicado @botozap/[email protected] contém as tools por stdio, mas ainda não contém o resource de Eventos nem o transporte remoto. O release candidate @botozap/[email protected] está fixado no commit 4b7f678 e passou typecheck, testes, build, Changesets e instalação dos tarballs em projeto limpo no gate da issue #222, mas ainda não foi publicado no npm. Até a publicação, use a versão publicada para tools e o checkout fixado abaixo somente para avaliar push/replay em localhost. Não atribua subscriptions ao pacote 0.1.0.

Matriz de compatibilidade

Leitura em 26/08/2026, nas versões indicadas. A primeira coluna registra se o cliente trata o sinal; a segunda exige releitura real do conteúdo, não apenas invalidar ou atualizar um catálogo. Nenhum dos hosts inspecionados transforma essa atualização em novo agent turn. Veja versões, hashes, comandos de inspeção e fontes primárias no relatório técnico da matriz.

ClienteNotification recebidaResource relidoNovo agent turnEvidência e uso recomendado
CodexNão com o BotoZapNãoNãoNo rust-v0.150.0, o cliente não chama resources/subscribe. Se receber um update, apenas o registra; não relê nem abre outro turno. fonte oficial.
Claude CodeNão trata resources/updatedNãoNãoNo 2.1.247, list_changed atualiza catálogos, mas resources/updated não é assinado nem tratado. Channels é outro mecanismo. docs oficiais.
CursorNão com o template atualNão; atualiza só o catálogoNãoNo 3.17.8, o host assina URIs concretas listadas. O BotoZap expõe apenas um template não enumerável; mesmo após update, o host não faz resources/read nem abre turno. mecanismo de wake separado.
Cliente de referênciaSim, handler observadoSim, chamada explícita no handlerSó se a aplicação enfileirarO SDK recebe notifications/resources/updated; o código do consumidor relê o resource e decide se cria trabalho. teste de contrato.

Para Codex, Claude Code e Cursor, configure as tools pelo quickstart da Central de Desenvolvedores. Quando o seu host não comprovar as três colunas, use Endpoint durável para disparar o worker; polling limitado fica apenas como fallback de desenvolvimento.

Quickstart 1 — stdio publicado (tools)

export BOTOZAP_API_KEY='bz_live_substitua_por_sua_chave'
pnpm dlx @botozap/[email protected]

O processo fala MCP em stdout e deve permanecer vivo enquanto o host usa as tools. Os snippets completos para Codex, Claude Code e Cursor ficam na Central de Desenvolvedores; a chave entra por variável de ambiente e nunca deve ser gravada no repositório.

Quickstart 2 — release candidate por stdio

git clone https://github.com/bytecraft-fernando/botozap-js.git
cd botozap-js
git checkout 4b7f67820f27887a5d1af78ad0fe90c5eb51de9c
pnpm install --frozen-lockfile
pnpm build

BOTOZAP_API_KEY='bz_live_substitua_por_sua_chave' node packages/mcp/dist/index.js

Esse checkout exato anuncia resources.subscribe, registra o template botozap://events{?after,limit} e emite notifications/resources/updated. O tail stdio existe só enquanto o processo e a sessão continuam abertos; executar uma tool e encerrar o servidor elimina o canal de push. A chave precisa incluir o escopo events:read no mesmo ambiente live ou Sandbox dos Eventos.

Quickstart 3 — Streamable HTTP remoto

BOTOZAP_MCP_TRANSPORT=streamable-http BOTOZAP_MCP_HOST=127.0.0.1 BOTOZAP_MCP_PORT=3001 BOTOZAP_EVENT_BUS_DATABASE_URL='postgresql://usuario:senha@host:5432/banco' node packages/mcp/dist/index.js

O endpoint stateful fica em http://127.0.0.1:3001/mcp. A BotoZap ainda não oferece uma URL MCP remota hospedada. Mantenha o processo em localhost e coloque um proxy com TLS, autenticação e allowlist de Origin na frente dele; o preview não valida esse header sozinho. Cada cliente ainda envia a própria chave BotoZap em Authorization: Bearer; a Conta e o ambiente vêm dessa credencial, nunca da URI. A conexão PostgreSQL precisa preservar LISTEN/NOTIFY, portanto use conexão direta ou pooler em modo de sessão. Esse comando é para quem opera o servidor remoto, não para expor a URL local sem proteção. O preview não oferece replay do protocolo por Last-Event-ID; depois de reconectar, recupere os Eventos pelo cursor BotoZap como na seção seguinte.

Quickstart 4 — subscription com cliente de referência

mkdir cliente-mcp-botozap && cd cliente-mcp-botozap
pnpm init
pnpm add @modelcontextprotocol/[email protected]
# salve o bloco abaixo como client.mjs
BOTOZAP_API_KEY='bz_live_substitua_por_sua_chave' BOTOZAP_MCP_URL='http://127.0.0.1:3001/mcp' BOTOZAP_EVENT_CURSOR='0' node client.mjs
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { ResourceUpdatedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";

const apiKey = process.env.BOTOZAP_API_KEY;
const mcpUrl = process.env.BOTOZAP_MCP_URL;
let cursor = process.env.BOTOZAP_EVENT_CURSOR ?? "0";
if (!apiKey || !mcpUrl) throw new Error("Defina BOTOZAP_API_KEY e BOTOZAP_MCP_URL");

const client = new Client({ name: "botozap-events", version: "1.0.0" });
const transport = new StreamableHTTPClientTransport(new URL(mcpUrl), {
  requestInit: { headers: { Authorization: "Bearer " + apiKey } },
});

async function read(uri) {
  const result = await client.readResource({ uri });
  const content = result.contents[0];
  if (!content || !("text" in content)) throw new Error("Resource sem JSON");
  return JSON.parse(content.text);
}

async function drain(uri) {
  do {
    const page = await read(uri.replace(/after=\d+/, "after=" + cursor));
    for (const event of page.data) {
      // Em produção: dedupe durável por event.id e enqueue do worker.
      console.log(JSON.stringify({ id: event.id, cursor: event.cursor, type: event.type }));
    }
    cursor = page.paging.cursor;
    console.error("salve BOTOZAP_EVENT_CURSOR=" + cursor);
    if (!page.paging.has_more) break;
  } while (true);
}

await client.connect(transport);
await drain("botozap://events?after=" + cursor + "&limit=100"); // catch-up
const subscribedUri = "botozap://events?after=" + cursor + "&limit=100";
let pendingDrain = Promise.resolve();
client.setNotificationHandler(ResourceUpdatedNotificationSchema, ({ params }) => {
  if (params.uri !== subscribedUri) return;
  pendingDrain = pendingDrain.then(() => drain(subscribedUri));
});
await client.subscribeResource({ uri: subscribedUri }); // resources/subscribe
console.error("assinatura ativa; Ctrl+C encerra");
await new Promise(() => {});

O handler serializa releituras, imprime somente identificadores do Evento e informa o novo paging.cursor em stderr. Em produção, persista esse cursor e deduplique por event.id antes de qualquer efeito. Substitua o console.log por enqueue durável; chamar o modelo diretamente dentro do handler torna replay, retry e deploy frágeis.

Cursor, replay e reconexão

  1. Carregue o último cursor confirmado; zero inicia desde o primeiro Evento retido.
  2. Leia botozap://events?after=<cursor>&limit=100 até paging.has_more ficar falso.
  3. Deduplique cada efeito por event.id e só então salve paging.cursor.
  4. Assine uma nova URI com o cursor final. O probe inicial fecha a corrida entre catch-up e subscribe.
  5. Trate notifications como hints best-effort: elas podem repetir ou se perder. A garantia vem do stream autoritativo e do replay, não do sinal.

Se o runtime precisa iniciar mesmo com o host MCP fechado, complete o quickstart do Endpoint durável. Ele valida HMAC sobre bytes crus, persiste antes do 2xx, deduplica retries e acorda um worker separado. O MCP reduz latência enquanto conectado; o Endpoint e o outbox mantêm a garantia operacional.

Fontes e prova local