npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zapcore-mcp

v0.1.1

Published

Servidor MCP (stdio) sobre a API REST v2 do zapCore — read-only por padrão, envio atrás de flag explícito + allowlist

Downloads

374

Readme

zapcore-mcp

Servidor MCP (stdio) sobre a API REST v2 do zapCore. Zero linha de Go — é um cliente HTTP; o motor não muda e não corre risco.

Desenho completo: docs/specs/2026-08-29-mcp-server-design.md.

O que ele expõe

8 tools de leitura (sempre): zap_session, zap_user_lookup, zap_contacts, zap_groups, zap_labels, zap_history, zap_message_status, zap_media_download.

4 tools de escrita (só com o flag): zap_send, zap_send_interactive, zap_react, zap_chat_action.

Doze tools cobrem ~30 rotas porque o discriminador mora no argumento, não no nome. Cada tool a mais pesa em TODA requisição da sessão — medido: leitura sozinha = 3,9 KB (~1k tokens); com escrita = 8,0 KB (~2k tokens). Uma tool por rota custaria mais de 10× isso e o servidor viraria algo que se desliga.

Requisitos

  • Node.js ≥ 20 (para instalar via npm) ou Docker — não precisa dos dois.
  • Um token de instância zapCore (ZAPCORE_TOKEN) e a URL da API (ZAPCORE_URL).
  • Se a base estiver atrás do Cloudflare Access: um service token do app zapcore-mcp.

Escrita é opt-in, e o padrão não sobe

ZAPCORE_MCP_WRITE=1   sem   ZAPCORE_MCP_ALLOW   →  o servidor NÃO INICIA

Sem ZAPCORE_MCP_WRITE=1 as tools de escrita não são registradas — não é recusa no handler, é ausência no tools/list. O modelo não tem como tentar enviar.

Com o flag ligado, ZAPCORE_MCP_ALLOW é o raio de alcance: mensagem só sai para os números/JIDs listados. As duas travas são independentes de propósito — uma é o interruptor, a outra é o alcance. A allowlist mora no env, fora do alcance da conversa: o modelo não consegue se autorizar.

Config (variáveis de ambiente)

| Env | Obrigatório | Default | Para quê | |---|---|---|---| | ZAPCORE_TOKEN | sim | — | header token da instância | | ZAPCORE_URL | não | https://zapcore.barberai.online | base da API — exige https://; http:// só em localhost/127.0.0.1/[::1] | | ZAPCORE_MCP_WRITE | não | (desligado) | 1/true/yes liga as 4 tools de envio | | ZAPCORE_MCP_ALLOW | se WRITE | — | destinos permitidos, E.164 separados por vírgula | | ZAPCORE_MCP_TIMEOUT_MS | não | 30000 | timeout por chamada | | ZAPCORE_MCP_DOWNLOAD_DIR | não | <TEMP>/zapcore-mcp | onde zap_media_download grava | | ZAPCORE_CF_ACCESS_CLIENT_ID | se atrás do Access | — | service token do Cloudflare Access | | ZAPCORE_CF_ACCESS_CLIENT_SECRET | se atrás do Access | — | par do anterior — os dois ou nenhum |

Config ausente/errada falha alto no stderr, com mensagem em português, e o processo sai com código 1 — nunca sobe "meio ligado".

Três formas de rodar

O pacote é publicado no npm como zapcore-mcp. O código-fonte é proprietário e o repositório é privado: o que vai ao registro é o JavaScript compilado, e mais nada. Use uma destas três:

(a) npx, sem instalar nada

npx -y zapcore-mcp

É a forma usada nos exemplos de configuração abaixo. Para fixar a versão: npx -y [email protected].

(b) Instalar global

npm install -g zapcore-mcp

Registra o binário zapcore-mcp no PATH global.

(c) Docker — forma canônica para distribuir sem expor código-fonte

docker run -i --rm \
  -e ZAPCORE_URL=https://zapcore.barberai.online \
  -e ZAPCORE_TOKEN=... \
  ghcr.io/adeiltonpessini/zapcore-mcp

-i é obrigatório: o servidor fala MCP por stdio, não abre porta nenhuma (sem -p). A imagem em ghcr.io/adeiltonpessini/zapcore-mcp ainda não foi publicada — o Dockerfile builda a partir do fonte, que é privado. Enquanto ela não existir, use (a) ou (b).

Configuração por cliente

Em todos os exemplos abaixo, troque ZAPCORE_TOKEN pelo token real e, se a base estiver atrás do Cloudflare Access, acrescente ZAPCORE_CF_ACCESS_CLIENT_ID e ZAPCORE_CF_ACCESS_CLIENT_SECRET (ver seção Access). Nunca cole o token na conversa com o modelo — ele vai para o env do processo, não para o prompt.

Claude Desktop

claude_desktop_config.json (Settings → Developer → Edit Config):

{
  "mcpServers": {
    "zapcore": {
      "command": "node",
      "args": ["/caminho/absoluto/para/zapcore/mcp/dist/bin.js"],
      "env": {
        "ZAPCORE_TOKEN": "...",
        "ZAPCORE_URL": "https://zapcore.barberai.online"
      }
    }
  }
}

Ou via Docker, sem precisar de Node instalado na máquina:

{
  "mcpServers": {
    "zapcore": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "ZAPCORE_URL", "-e", "ZAPCORE_TOKEN",
        "ghcr.io/adeiltonpessini/zapcore-mcp"
      ],
      "env": {
        "ZAPCORE_TOKEN": "...",
        "ZAPCORE_URL": "https://zapcore.barberai.online"
      }
    }
  }
}

Claude Code

CLI (grava em ~/.claude.json do projeto atual):

claude mcp add zapcore -e ZAPCORE_TOKEN=... -e ZAPCORE_URL=https://zapcore.barberai.online \
  -- node /caminho/absoluto/para/zapcore/mcp/dist/bin.js

Ou .mcp.json na raiz do projeto (compartilhável no git sem o token — prefira um wrapper que injete o segredo, como mcp/scripts/zapcore-mcp.ps1 faz para o time interno lendo do DPAPI):

{
  "mcpServers": {
    "zapcore": {
      "command": "node",
      "args": ["/caminho/absoluto/para/zapcore/mcp/dist/bin.js"],
      "env": { "ZAPCORE_TOKEN": "...", "ZAPCORE_URL": "https://zapcore.barberai.online" }
    }
  }
}

No repo interno, registre pelo wrapper — ele decifra o token do DPAPI na hora e nunca escreve segredo em disco:

claude mcp add zapcore -- powershell -NoProfile -ExecutionPolicy Bypass \
  -File f:/zapcore/mcp/scripts/zapcore-mcp.ps1

Cursor

.cursor/mcp.json (no projeto) ou ~/.cursor/mcp.json (global):

{
  "mcpServers": {
    "zapcore": {
      "command": "node",
      "args": ["/caminho/absoluto/para/zapcore/mcp/dist/bin.js"],
      "env": {
        "ZAPCORE_TOKEN": "...",
        "ZAPCORE_URL": "https://zapcore.barberai.online"
      }
    }
  }
}

VS Code (GitHub Copilot Chat)

.vscode/mcp.json, usando inputs para não deixar o token em texto puro no arquivo versionado — o VS Code pergunta uma vez e guarda no cofre de segredos:

{
  "inputs": [
    {
      "type": "promptString",
      "id": "zapcore-token",
      "description": "Token da instância zapCore",
      "password": true
    }
  ],
  "servers": {
    "zapcore": {
      "type": "stdio",
      "command": "node",
      "args": ["/caminho/absoluto/para/zapcore/mcp/dist/bin.js"],
      "env": {
        "ZAPCORE_TOKEN": "${input:zapcore-token}",
        "ZAPCORE_URL": "https://zapcore.barberai.online"
      }
    }
  }
}

Windsurf

~/.codeium/windsurf/mcp_config.json (Windsurf Settings → Cascade → MCP Servers → View raw config):

{
  "mcpServers": {
    "zapcore": {
      "command": "node",
      "args": ["/caminho/absoluto/para/zapcore/mcp/dist/bin.js"],
      "env": {
        "ZAPCORE_TOKEN": "...",
        "ZAPCORE_URL": "https://zapcore.barberai.online"
      }
    }
  }
}

Ligar a escrita

Em qualquer cliente acima, acrescente ao env:

"ZAPCORE_MCP_WRITE": "1",
"ZAPCORE_MCP_ALLOW": "5527997491287"

ZAPCORE_MCP_ALLOW aceita mais de um destino separado por vírgula. Sem ele o processo recusa a subir — não existe "escrita aberta por esquecimento".

Catálogo de tools

Leitura (sempre disponíveis)

| Tool | Parâmetros principais | Pergunta de exemplo | |---|---|---| | zap_session | (nenhum) | "A sessão do WhatsApp está conectada?" | | zap_user_lookup | phones[], fields[] (check/info/avatar/lid), preview? | "Esse número tem WhatsApp? Qual o nome do perfil?" | | zap_contacts | limit?, offset?, search? | "Procura na agenda um contato chamado Marcos" | | zap_groups | action (list/info/invitelink), groupJID? | "Lista os grupos da instância" | | zap_labels | action (list/targets), labelId?, jid? | "Quais conversas estão marcadas com a etiqueta VIP?" | | zap_history | chatJid (ou "index"), limit?, cursor? | "Mostra as últimas mensagens dessa conversa" | | zap_message_status | messageId | "Essa mensagem já foi lida?" | | zap_media_download | kind, mediaKey, fileEncSHA256, fileSHA256, url?/directPath? | "Baixa a imagem que esse cliente mandou" |

Escrita (só com ZAPCORE_MCP_WRITE=1)

| Tool | Parâmetros principais | Pergunta de exemplo | |---|---|---| | zap_send | to, kind (text/image/audio/video/document/sticker/location/contact/ptv), body?, media? (data:), fileName? | "Manda um texto de confirmação para esse cliente" | | zap_send_interactive | to, kind (buttons/list/poll/carousel), buttons?/sections?/options?/cards?, degrade? | "Manda uma enquete perguntando o horário preferido" | | zap_react | to, messageId, emoji ("" remove) | "Reage com 👍 na última mensagem dele" | | zap_chat_action | to, action (markread/archive/pin/mute/star/unread/delete), messageId?, value? | "Marca essa conversa como lida" |

Detalhe completo de cada schema está em src/tools/read.ts e src/tools/write.ts (comentado, é a fonte de verdade — este catálogo é o resumo).

Segurança

  • O token nunca deve estar no prompt da conversa — só no env do processo MCP. Um cliente que só aceita colar variáveis no chat não é seguro para isto.
  • Read-only por padrão: instalar sem ZAPCORE_MCP_WRITE=1 é criptograficamente incapaz de enviar mensagem — as tools de escrita não existem no tools/list, não é uma checagem que pode falhar.
  • A allowlist (ZAPCORE_MCP_ALLOW) mora fora do alcance do modelo: mesmo com escrita ligada, o modelo não pode se autoconceder um novo destino — só quem controla o processo (o env) decide isso.
  • Em máquina de pessoa, prefira um wrapper que leia o token de um cofre (DPAPI, Keychain, pass) e injete no processo filho, em vez de gravar em .mcp.json/claude_desktop_config.json em texto puro — é o que mcp/scripts/zapcore-mcp.ps1 faz para o time interno.

Cloudflare Access: o hostname público é protegido

zapcore.barberai.online fica atrás do Access. Sem credencial a chamada nem chega no zapcore: a borda devolve 302 para a tela de login e o corpo é HTML. Por isso o cliente usa redirect: 'manual' e traduz esse 302 em access_login_required, com a instrução — em vez do genérico "resposta não-JSON", que não diz o que fazer.

Para rodar o MCP fora do servidor, use um service token do Access autorizado no app zapcore (policy zapcore-mcp (service token), decisão non_identity):

"env": {
  "ZAPCORE_TOKEN": "...",
  "ZAPCORE_CF_ACCESS_CLIENT_ID": "....access",
  "ZAPCORE_CF_ACCESS_CLIENT_SECRET": "..."
}

Os dois andam juntos: com só um, loadConfig recusa a subir — metade da credencial vira um 302 confuso lá na frente. Dentro do servidor (netns do container, ZAPCORE_URL= http://127.0.0.1:8080) não passa pela borda e nenhum dos dois é necessário.

Na máquina do dono os segredos ficam em DPAPI, nunca em .txt: zapcore-instance-token, zapcore-mcp-cf-client-id, zapcore-mcp-cf-client-secret (leitura: ~/.secrets/ler-segredo.ps1 <nome>).

As quatro travas que vieram de erro medido em produção

Cada uma é código, não texto de prompt — o modelo não contorna.

  1. Mídia só por data:. O container do zapcore não tem egress: imagem por URL externa falha lá dentro. O wrapper recusa a URL com a instrução, em vez de deixar o servidor devolver um erro obscuro.
  2. Carrossel exige imagem em todo card. Card sem image quebra a bolha INTEIRA no WhatsApp Desktop/Web e abre normal no celular (carousel.go:23) — metade dos destinatários vê lixo, o que é pior que falhar. Use degrade:"text" para cair em texto simples de propósito.
  3. Enquete usa group, nunca Phone — inclusive para número individual. O servidor ainda erra com o typo herdado do projeto de origem (missing Grouop in payload), que manda procurar um campo inexistente; o wrapper traduz.
  4. POST de envio não é repetido automaticamente. Timeout de rede não prova que a mensagem não saiu; reenviar duplica no aparelho de quem recebe. Só GET tem retry.

Fora de escopo, de propósito

/status/send/* (story vai para a lista de contatos inteira — allowlist de destino não protege), criação/remoção de grupo, gestão de usuários da instância, e o /session/events (stream). Cada um é raio de explosão que uma tool de agente não deve ter.

Verificação

npm run typecheck && npm run build && npm test

44 testes: as guardas, o mapeamento de erro da v2, o payload que sai na rede em cada kind, e a asserção central — sem ZAPCORE_MCP_WRITE=1 nenhuma tool de escrita é registrada.

Verificação em produção (o que build verde não prova)

A borda pública (zapcore.barberai.online) está atrás do Cloudflare Access — a máquina de dev leva 302 para o login, então não dá para testar de fora. E o serviço não publica porta: no Swarm, Endpoint.Ports = null. O jeito de exercitar o cliente de verdade é rodar dentro do netns do container, onde a API é http://127.0.0.1:8080:

# no servidor, com dist/ + node_modules/zod em /tmp/zapcore-mcp
PG=$(docker ps -q -f name=postgres | head -1)
TOKEN=$(docker exec $PG psql -qtAX -U zapcore -d zapcore \
  -c "select token from users where connected=1 limit 1")
CID=$(docker ps -q -f name=zapcore_zapcore)

docker run --rm --network container:$CID -v /tmp/zapcore-mcp:/app \
  -e ZAPCORE_URL=http://127.0.0.1:8080 -e ZAPCORE_TOKEN="$TOKEN" \
  -e ZAPCORE_MCP_WRITE=1 -e ZAPCORE_MCP_ALLOW=5527997491287 \
  node:22-alpine node /app/scripts/verifica-producao.mjs

scripts/verifica-producao.mjs roda os handlers do MCP, não curl: sessão, lookup, texto, imagem base64, enquete (prova o campo group) e carrossel de 2 cards com imagem — mais as três guardas, que precisam recusar antes da rede. Executado em 30/08/2026 contra produção: 9/9 OK, mensagens entregues no 5527997491287.

Publicar (procedimento)

O pacote npm é a distribuição oficial. package.json já traz files (só .js compilado), bin, engines, prepack (roda o build) e publishConfig: { access: public, provenance: false }proveniência fica desligada: ela exige repositório público no GitHub, e ligar faria todo publish falhar com uma mensagem que convida a abrir o código.

homepage e bugs apontam para https://zapcore.app, não para o repositório: todo link da página do npm é público, e o repo é privado. repository foi removido pelo mesmo motivo. O test/publicacao.test.ts falha se algum voltar a apontar para o GitHub.

cd mcp
npm login                 # conta do dono; 2FA pede OTP no publish
npm run build && npm test
npm pack --dry-run        # confira o conteúdo: só dist/*.js, README, server.json
npm publish               # access/provenance já vêm do publishConfig

Depois que a versão existir no npm, o registro oficial do MCP (mcp/server.json, já no formato do MCP Registry):

mcp-publisher login github     # namespace io.github.adeiltonpessini
mcp-publisher publish

A versão de server.json (topo e dentro de packages[].version) tem que ser a mesma já publicada no npm — o registro valida que o pacote existe.

Para a imagem Docker: o Dockerfile builda a partir do fonte privado; falta um workflow que faça docker build + docker push ghcr.io/adeiltonpessini/zapcore-mcp.