
Security News
Happy Birthday, Shai-Hulud
It has been one year since Shai-Hulud made its first appearance on npm.
zapcore-mcp
Advanced tools
Servidor MCP (stdio) sobre a API REST v2 do zapCore — read-only por padrão, envio atrás de flag explícito + allowlist
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.
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.
ZAPCORE_TOKEN) e a URL da API (ZAPCORE_URL).zapcore-mcp.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.
| 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".
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:
npx, sem instalar nadanpx -y zapcore-mcp
É a forma usada nos exemplos de configuração abaixo. Para fixar a versão:
npx -y zapcore-mcp@0.1.0.
npm install -g zapcore-mcp
Registra o binário zapcore-mcp no PATH global.
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).
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_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"
}
}
}
}
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/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"
}
}
}
}
.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"
}
}
}
}
~/.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"
}
}
}
}
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".
| 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" |
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).
env do processo
MCP. Um cliente que só aceita colar variáveis no chat não é seguro para isto.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.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.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.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>).
Cada uma é código, não texto de prompt — o modelo não contorna.
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.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.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.GET
tem retry./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.
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.
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.
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.
FAQs
Servidor MCP (stdio) sobre a API REST v2 do zapCore — read-only por padrão, envio atrás de flag explícito + allowlist
The npm package zapcore-mcp receives a total of 0 weekly downloads. As such, zapcore-mcp popularity was classified as not popular.
We found that zapcore-mcp demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.