[npm]
[License: MIT]
[M8ven Score]
wazap is an MCP server for your own WhatsApp account. It links as a device,
the way WhatsApp Web does, keeps what it sees on your machine, and gives any
MCP client — Claude, ChatGPT, Gemini, Cursor, Codex — 23 tools over your chats,
messages, media, contacts and groups.
You stop opening WhatsApp to find things and ask instead:
- "What did I miss on WhatsApp today?" — one answer across every linked
account: who is waiting on you, mentions, missed calls, busy groups condensed.
- "Find the invoice Dan sent in spring." — search by meaning and by words at
once, then open the file.
- "What did the voice note from mama say?" — voice notes come back as text.
- "Tell Ana I'll be twenty minutes late." — you see the recipient and the
exact words first. Nothing leaves until you say yes.
No browser, no business account, no third-party service. There is no wazap
cloud: it runs where you run it.
Get started
You need Node.js 22.16+ on the 22 line, or 24+, and a phone with WhatsApp.
setup links your account, finds the MCP clients installed on this machine
(Claude Code, Claude Desktop, Cursor, Codex, Gemini CLI), writes their config,
installs the five skills where the client reads them, and tells you what to
restart. Then ask your assistant: "what did I miss on WhatsApp today?"
Rather let your agent do it? Paste this into it:
Set up WhatsApp for me: run npx wazap-mcp setup --agent and follow what it prints.
npx wazap-mcp on its own, in a terminal, starts nothing: it prints where you
stand and the next command. When something is off, npx wazap-mcp status is
the first thing to run; it prints a fix next to anything broken.
Per client
[Install in Cursor]
The hand-written entry, for any client that takes the usual JSON:
Every step setup takes, each file connect writes, and upgrading are in
docs/install.md.
Linking your account
Linking is a one-time step. setup does it for you; on its own it is:
With --phone, wazap prints an 8-character code. On the phone, go to
WhatsApp → Settings → Linked devices → Link a device → Link with phone number
instead and type it in. The pairing code works over SSH and on a headless
server, where a QR is impractical.
An assistant can link the account itself, too: the link_account tool returns
the same code, so a Claude Desktop user never needs a terminal.
Several WhatsApp accounts can live in one wazap (wazap account add <id>, then
wazap login --account <id>); see docs/accounts.md.
The 23 tools
What every argument means, and every error code, is in
docs/tools.md. The assistant gets all of it by calling learn.
Scheduled messages and reminders. send_message with send_at ("tomorrow
at 9" as 2026-10-12T09:00 in Europe/Bucharest) drafts a message for later;
the user's yes to that preview, time included, schedules it, and every send
check runs again when it goes. One wazap could not send within 15 minutes of
its time is reported as missed, never sent late. reminder keeps "remind me if
Ana has not answered in 2 days" and posts it to your webhook when due. Both
survive restarts; see Scheduling a message
and Reminders.
Two features are off by default and can run entirely on your machine:
voice transcription with whisper.cpp (docs/voice.md) and
semantic recall, so search matches meaning and not only words
(docs/recall.md). Turn them on with
wazap config transcribe local and wazap config recall local.
Skills
Tools say what is possible; the five Agent Skills in
skills/ say how to do the job well.
setup installs them for Claude Code, Codex and Cursor
(npx wazap-mcp skills install <harness> does it by hand). Every other client
gets the same five as MCP prompts from the server, with nothing to install.
Transports
stdio is the default: the client starts npx -y wazap-mcp and talks to it
over stdin/stdout. Nothing listens on the network.
HTTP (Streamable HTTP) is for a server that outlives the client, or one
another machine reaches:
--host/--port (or WAZAP_HOST/WAZAP_PORT) move it. wazap refuses to bind
anything but loopback without WAZAP_READ_TOKEN. Clients authenticate with a
static bearer token (WAZAP_READ_TOKEN, plus WAZAP_WRITE_TOKEN for the write
tools) or with OAuth 2.1, which wazap serves itself. Details in
docs/self-host.md.
Remote access
Two commands keep wazap running and reachable from hosted agents:
expose prints the URL and a consent password. Give the agent only the URL;
it registers itself, and the consent page asks you for the password and
whether that agent may only read or also send. See
Keep it running.
The service recovers by itself: a dropped link is retried with backoff, a
watchdog replaces a socket that went dead across a sleep or stopped answering,
and when reconnecting keeps failing wazap exits so launchd or systemd starts it
fresh. The linked session is never deleted for any of that; only WhatsApp
unlinking the device needs a new link. Set WAZAP_WEBHOOK_EVENTS to include
alert to be told when the link has been down for 5 minutes, when it is back,
and when it needs linking again (Link alerts);
get_status shows the same under link.
Deployment
On a server of your own, the session never leaves it. With Docker (images for
amd64 and arm64 at ghcr.io/razvangirgiz/wazap):
The container publishes port 8766 on loopback only; put TLS in front
(deploy/Caddyfile is two lines). A systemd unit is in
deploy/wazap.service. Both setups, reverse-proxy
trust and the OAuth server are in docs/self-host.md.
Calling wazap from your own product, and receiving signed webhooks for incoming
messages, is in docs/api-and-webhooks.md.
Configuration
Every setting is optional. They are environment variables, read from the
process or from ~/.wazap/.env (a real environment variable wins, a flag wins
over both). You rarely edit them by hand: wazap config shows the effective
settings and changes one.
The full list, including the webhook filters, kept-media limits, retention and proxy trust, is in
docs/settings.md; .env.example is the
same list as a commented file.
Security and privacy
- Nothing is sent without a yes.
send_message only drafts: it returns the
recipient and the exact text and touches no network. Only confirm_send
sends, and a draft goes out at most once, even across a crash. Draft-then-
confirm is a workflow, not proof of consent: an agent can call both tools
unless your client makes you approve the second one.
- Read-only is a real switch. With
WAZAP_READ_ONLY=1, --read-only or
wazap config writes off, the write tools are not registered, so the
assistant cannot message anyone even by mistake. The Claude Desktop bundle
ships with read-only ticked.
- Recipients can be pinned.
wazap config send allow +15550100,… limits an
account to a list, deny refuses the ones named; both are checked at draft and again at
send (docs/send-rules.md).
- Writes are rate limited, 20 a minute per account by default, because
sending faster than a human is how numbers get banned.
- Your data stays on your machine. Credentials, messages, media and notes
live in
~/.wazap (0700, credential files 0600). No account, no telemetry.
What leaves the machine: WhatsApp traffic; the npm registry, for the version
check; Hugging Face, when you download a transcription or embedding model; a
transcription API, only if you chose openai; your own webhook, if you set
one (docs/data.md).
- Some people can be kept out. Tag a contact
#private and their words stay
out of everything the assistant did not ask about them by name, and out of
the webhook. #no-catchup keeps their chat out of catch_up.
- What it does not do. No bulk send, no scheduler, no campaigns. It never
marks anything read on its own, and sends text without link previews, so
nothing fetches the pages you link.
Threat model, findings and the limits of each of these:
docs/security-audit.md.
Known limitations
- The protocol is unofficial. wazap is built on
Baileys, a reverse-engineered
implementation of WhatsApp's multi-device protocol. It is not the WhatsApp
Business API and Meta does not support it. A protocol change can break wazap
until Baileys catches up, and an account that sends in bulk or to people who
did not ask can be restricted or banned. The safeguards above reduce that
risk; they do not remove it, and the risk is yours.
- Model behaviour is measured on Claude only. The evaluation in
eval/
(58 cases, each run three times) has been run on Claude. Other clients get
the same tools and the same server-side guards, but nobody has measured how
their models use them. The manual protocol for ChatGPT is
eval/chatgpt-protocol.md.
- History is what the phone syncs to a linked device, not your whole
archive.
read_messages with before asks the phone for more, within what
WhatsApp still keeps.
- Old media expires. WhatsApp drops old attachments from its servers;
get_media then returns MEDIA_UNAVAILABLE, unless wazap
kept a copy when it arrived. By default it keeps
the photos, voice notes, audio and documents of unmuted chats, up to 16 MB
each, 1 GB in all and 90 days, while wazap serve runs; media that arrived
and expired while nothing was keeping it cannot be brought back.
- Names come from the phone's address book, which WhatsApp only sends to a
fresh connection. If contacts read as numbers (
contacts_named: 0 in
get_status), wazap contacts resync asks again while no server runs.
@lid ids. Newer accounts are addressed by a privacy id; wazap maps it
back to a number when it has learned the mapping.
- Calls are WhatsApp calls only, and a call that starts and ends while
wazap is stopped may be missing. Cellular calls are never visible.
- The phone must stay reachable. A linked device stops receiving once the
phone has been offline too long;
get_status says so.
What the 1.x line promises not to break is in docs/stability.md.
Documentation
Contributing
The tests need no WhatsApp account. CLAUDE.md is the contributor's
map: layout, the rules that must never break, and the release procedure.
Issues and pull requests are welcome at
github.com/razvangirgiz/wazap.
License
MIT © Răzvan Girgiz