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
Maintainers
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 INICIASem 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-mcpRegistra 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.jsOu .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.ps1Cursor
.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
envdo 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 notools/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.jsonem texto puro — é o quemcp/scripts/zapcore-mcp.ps1faz 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.
- 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. - Carrossel exige imagem em todo card. Card sem
imagequebra 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. Usedegrade:"text"para cair em texto simples de propósito. - Enquete usa
group, nuncaPhone— 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. - 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ó
GETtem 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 test44 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.mjsscripts/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 publishConfigDepois 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 publishA 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.
