wa-web-mcp
wa-web-mcp
MCP server that lets Claude read and send WhatsApp for your team's personal number, driving the genuine WhatsApp Web client (via WPPConnect) behind a code-enforced human-behavior middleware.
Setup / template note: this is a shareable copy with infrastructure specifics replaced by placeholders —
YOUR_TAILNET_IP,your-host,your-tailnet.ts.net, unix useryouruser, bundle idcom.example.wa-web-mcp. Search-and-replace those (or drive them viaWA_WEB_URL/ env) for your own host before deploying. Config lives inconfig.sample.json(copy toconfig.json); nothing sensitive ships in the repo.
Built after two WhatsApp restrictions from an unofficial whatsmeow bridge. Two principles (see SPEC.md):
Real client, not forged. WPPConnect runs actual WhatsApp Web, so the on-sight fingerprint ban vector that killed the whatsmeow bridge is closed.
Guards in code, not in the prompt. Every limit is service-side middleware the model cannot talk past.
Status
Layer | State |
Human-behavior middleware (all gates, reciprocity governor, warm-up) | built + unit-tested (38 tests) |
WPPConnect client wrapper, outbound ritual executor | built, compiles; runtime-validated on the host |
MCP HTTP server + tools + override header | built, compiles; runtime-validated on the host |
Deploy (m2 LaunchDaemon) | artifacts ready in |
The safety-critical core is proven with deterministic tests. The WPPConnect + MCP-transport glue compiles and follows the verified v2.2.1 / SDK 1.29 APIs, but end-to-end behavior is validated on first deploy (needs a browser + a linked session + a live MCP client — none reproducible in CI here).
Layout
src/
config.ts defaults (SPEC §6) + JSON override loader; dryRun starts TRUE
core/ clock (injectable), logger (decision log), tz helpers
state/db.ts SQLite: messages, new_contact_log (reciprocity), kv singletons
middleware/
ratelimit.ts interval + hourly + usync (peek/commit)
text.ts normalize + token-set-Jaccard similarity (dedup)
pipeline.ts the 9 gates + reciprocity governor + warm-up ramp + health
wa/
jids.ts @c.us / @g.us normalisation
client.ts WPPConnect wrapper: send/read/typing/presence + inbound/ack events
outbound.ts dry-run + serialised read→type→send ritual
mcp/server.ts tools + Streamable HTTP transport (bearer) + override header
index.ts wiringRun locally (dry-run, no send)
npm install # local: set PUPPETEER_SKIP_DOWNLOAD=true (browser only needed on the host that links)
npm run build
npm test # 38 unit testsDeploy (m2)
rsyncthis repo tom2:~/wa-web-mcp/(exclude node_modules/dist/store).Put
deploy/token.txt(a bearer, e.g.openssl rand -hex 32) andconfig.json(fromconfig.sample.json) in place.ssh -t m2 "bash ~/wa-web-mcp/deploy/install-m2.sh"— installs deps (incl. Chromium), builds, starts the LaunchDaemon.tail -f ~/wa-web-mcp/store/daemon.log, scan the QR once.Watch decisions in dry-run. When satisfied, set
config.jsondryRun:falseand re-run the installer. Reads/warm replies first; new-contact outreach ramps from 2/day.
Point Claude Code at http://YOUR_TAILNET_IP:8765/mcp with the bearer.
Safety controls
dryRun(config): evaluate + log, never send. Default true.halt_outbound/resume_outboundtools: persisted kill switch, never bypassable.X-Rate-Limit-Overrideheader: operator bypass of pacing gates (not kill switch, not dedup). LLM cannot set it.get_send_healthtool: live posture (caps, reply rate, warm-up, cooldown).
Sending a local file — from any Claude Code machine
Text sends are just the send_message tool. Sending a local file needs the
bytes to reach the daemon first, so bin/wa-send-file does upload (POST /media) + send_file in one shot, entirely over the daemon's HTTP endpoint
(no scp/SSH). Every send still passes the full server-side pipeline.
wa-send-file <local-file> <recipient> [caption]recipient is a chat id — E.164 digits, <digits>@c.us, <id>@lid, or a
<id>@g.us group. Resolve a contact name to its id with the list_chats
tool first. A bare number is resolved through the daemon's resolveTarget
path before send, so first-contact sends can work without a pre-existing chat
when WhatsApp's lookup returns a canonical id.
Set up a new machine (any Mac on the tailnet with Claude Code):
# 1. put the script on PATH
install -m 0755 bin/wa-send-file /usr/local/bin/wa-send-file
# 2. drop the bearer token where the script looks for it
mkdir -p ~/.config/wa-web && printf '%s' '<TOKEN>' > ~/.config/wa-web/token && chmod 600 ~/.config/wa-web/tokenToken lookup order: WA_WEB_TOKEN env → ~/.config/wa-web/token →
./deploy/token.txt. Base URL override: WA_WEB_URL. Operator bypass for one
call: WA_WEB_OVERRIDE=1. Exit codes: 0 sent, 3 held by a gate, 1 error.
The controls are the same everywhere because they live in the one daemon — every machine is a thin client of it. Don't run a second daemon/linked device.
Limitations / follow-ups
resolveTargetfalls back to the raw jid, with a warning log, whencheckNumberStatusfails or reports no canonical id. A first-contact send can still target a stale jid if WhatsApp's lookup path is down.Staged uploads accumulate under
store/uploads/; swept every 30 minutes (TTL 1h).quietHours.outsideBehavior: 'defer'is declared but not implemented — quiet-hours holds reject and rely on the queue to retry transient gates.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/myi1/wa-web-mcp-oss'
If you have feedback or need assistance with the MCP directory API, please join our Discord server