WhatsApp MCP Server
A single-binary Go MCP server that wraps whatsmeow to expose a personal WhatsApp account to LLMs. whatsapp-mcp serve runs as a lightweight HTTP daemon on 127.0.0.1:8765; MCP clients (Claude Desktop, Cursor, Claude Code, etc.) connect to it via HTTP — no process spawning, no stdin/stdout juggling. Messages are cached in local SQLite and only travel to the model when the agent calls a tool.
Unaffiliated. This is an independent open-source project. It is not affiliated with, endorsed by, or otherwise associated with Meta Platforms, Inc., WhatsApp, or whatsmeow. "WhatsApp" is a trademark of Meta Platforms, Inc., used here nominatively to describe interoperability.
This started as a fork of lharries/whatsapp-mcp and has since been rewritten as a single Go binary. What it adds over the original:
- LID resolution — normalises
@lidJIDs to real phone numbers for accurate contact matching. - Sent-message storage — outgoing messages are persisted locally so conversation history stays complete.
- Disappearing-message timers — outgoing messages inherit the group chat's ephemeral timer automatically.
- Targeted history sync — on-demand per-chat backfill via the
request_synctool. - Extended tool surface — 42 tools (see below): reactions, replies, edits, revoke, mark-read, typing, is-on-whatsapp, full group admin, blocklist, polls (create + vote + tally), contact cards, view-once flag, presence, privacy settings, and the profile "About" text.
- Single-instance enforcement — a
flock(2)onstore/.lockprevents twoserveprocesses racing on the same SQLite files.
Setup
Prerequisites
- Go 1.26+ (build-time only; runtime needs just the compiled binary).
- An MCP client that speaks HTTP (Claude Desktop, Cursor, Claude Code, etc.).
- FFmpeg (optional) — required only for
send_audio_messagewhen the input is not already.oggOpus. Without it, usesend_fileto send raw audio. - Windows: CGO must be enabled — see docs/windows.md.
Install
git clone https://github.com/Sealjay/mcp-whatsapp.git
cd mcp-whatsapp
make build # writes ./bin/whatsapp-mcp
Pair your phone (first run only)
Start the daemon, then open the pairing page in a browser:
./bin/whatsapp-mcp serve # starts on 127.0.0.1:8765
open http://127.0.0.1:8765/pair # macOS; or visit the URL manually
Scan the QR with WhatsApp (Settings → Linked Devices → Link a Device). The session persists to ./store/whatsapp.db; WhatsApp rotates it roughly every 20 days, when /pair shows a fresh QR.
- No browser:
./bin/whatsapp-mcp loginrenders the QR in the terminal. - No QR scan: use the phone-number form on
/pair(orlogin -phone +<international number>), then type the code into Link a Device → Link with phone number instead. - The
/pair/*endpoints are CSRF-protected and rate-limited (15 GET/min, 1 reset/min, 3 code requests/min).
Connect your MCP client
serve listens on 127.0.0.1:8765 (or $WHATSAPP_MCP_ADDR) and serves MCP at /mcp. It is stateless (no Mcp-Session-Id) and speaks MCP 2026-07-28 as well as older revisions.
// Claude Code (.claude/mcp.json or ~/.claude/mcp.json) and Cursor (~/.cursor/mcp.json)
{ "mcpServers": { "whatsapp": { "type": "http", "url": "http://127.0.0.1:8765/mcp" } } }
// Claude Desktop only launches stdio servers, so bridge with mcp-remote
// (~/Library/Application Support/Claude/claude_desktop_config.json)
{ "mcpServers": { "whatsapp": { "command": "npx", "args": ["-y", "mcp-remote", "http://127.0.0.1:8765/mcp"] } } }
Restart the client. The daemon outlives client restarts.
Sending and receiving files
WHATSAPP_MCP_MEDIA_ROOT (default <store>/uploads/, created on first run) is the only directory file tools may touch:
- Sending:
send_fileandsend_audio_messagetake amedia_pathunder the root. - Receiving:
download_mediacaches to<store>/<chat_jid>/<hex(message_id)>/<safe_filename>(checked against the stored size and SHA-256). An optionaloutput_pathalso places the file under the root; an existing file there makes the call a no-op.
Paths outside the root, including symlinks that resolve outside it, are rejected. Anything inside the root is readable and writable by the tools, so keep secrets out.
Sandboxed clients (Claude Cowork and similar) cannot read the daemon's cache. Point WHATSAPP_MCP_MEDIA_ROOT at a directory the sandbox can read and give the client this instruction:
When calling the WhatsApp MCP's
download_media, always passoutput_pathset to a path under your shared workspace. Without it the decrypted file lands only in the daemon's local cache, which is outside your sandbox and unreadable.output_pathmust live underWHATSAPP_MCP_MEDIA_ROOTon the daemon side; the basename is yours to pick.
Serve flags and environment
-addr host:port(envWHATSAPP_MCP_ADDR, default127.0.0.1:8765).-allow-remote: bind a non-loopback address. RequiresWHATSAPP_MCP_TOKEN, a bearer token for/mcpand/pair/*;serveexits without it.WHATSAPP_MCP_MEDIA_ROOT: the file allowlist root above.WHATSAPP_MCP_DEBUG=1or-debug: verbose logs (see Debug logging).
Running the daemon
Events reach SQLite only while serve runs. After downtime, whatsmeow backfills via HistorySync within WhatsApp's own retention window; anything older is lost, and request_sync backfills one chat on demand. A flock(2) on store/.lock allows one serve (or login) per store.
Docker. The image (debian:bookworm-slim plus ffmpeg, a few hundred MB) runs -store /home/app/store; persist that volume. Pair with a one-off login container first, because login and a running serve cannot share the store lock:
docker build -t whatsapp-mcp --build-arg VERSION=$(git describe --tags --always) .
docker run --rm -it -v whatsapp-mcp-store:/home/app/store whatsapp-mcp login
TOKEN=$(openssl rand -hex 32); echo "$TOKEN"
docker run -d --name whatsapp-mcp -v whatsapp-mcp-store:/home/app/store \
-p 127.0.0.1:8765:8765 -e WHATSAPP_MCP_TOKEN="$TOKEN" \
whatsapp-mcp serve -addr 0.0.0.0:8765 -allow-remote
Clients then send Authorization: Bearer $TOKEN. The container binds loopback by default, so -allow-remote plus a token is what makes -p work. To re-pair after WhatsApp rotates the session (about every 20 days), stop the container, rerun the login container and start it again.
Architecture
cmd/whatsapp-mcp/ login / serve / smoke subcommands
internal/client/ whatsmeow client wrapper (send, download, events, history, features)
internal/daemon/ HTTP server, pairing state machine, /pair endpoints
internal/mcp/ mark3labs/mcp-go v1 server + tool registrations
internal/media/ ogg parsing, waveform synthesis, ffmpeg shell-out
internal/security/ path allowlisting, filename sanitisation, log redaction
internal/store/ SQLite cache, LID resolution, query layer
Data lives under ./store/ (-store DIR): messages.db (message cache), whatsapp.db (whatsmeow session) and .lock. Query tools read SQLite; send and action tools call whatsmeow; incoming events are written to SQLite by a goroutine in the same process.
Tools
42 tools, grouped by purpose.
Read / query
Send
Message actions
Groups
Blocklist
Privacy / presence / status
Admin
Deferred
Intentionally not exposed yet:
subscribe_presence— no persistence layer for presence events, skipped to avoid a dangling tool.- Profile photo setter — upstream whatsmeow doesn't expose a user-level setter.
- Approval-mode participants, communities, newsletters — low-use surface, deferred.
Limitations
- Prompt-injection risk: as with many MCP servers, this one is subject to the lethal trifecta. Prompt injection in incoming messages could lead to private data exfiltration — treat the tool surface accordingly.
- Message gaps: see Running the daemon.
- Single instance per store: only one
whatsapp-mcp servecan hold the store lock. Parallel MCP clients must point at different-storedirectories (and therefore different paired sessions). - Windows: requires CGO and a C compiler — see docs/windows.md.
- Upstream bounds: message fetch/send is bounded by what whatsmeow supports against the WhatsApp web multidevice API.
Development
make test # unit tests
make test-race # with -race
make vet # go vet
make e2e # build + JSON-RPC smoke over HTTP (requires -tags=e2e)
make smoke # boot-test the server without connecting to WhatsApp
Upgrading whatsmeow
Weekly CI runs an upstream upgrade probe. To do it manually:
make upgrade-check
This bumps go.mau.fi/whatsmeow@main, re-tidies, builds, and tests. If green, commit the go.mod / go.sum changes.
scripts/mdtest-parity.sh in CI fails the build early if upstream removes or renames any whatsmeow method we call — it's the canary for API drift.
Troubleshooting
connect failed …onserve— the daemon is not paired. Openhttp://127.0.0.1:8765/pairin a browser and scan the QR. Alternatively, run./bin/whatsapp-mcp loginin a terminal.another whatsapp-mcp instance is already running— only oneservecan hold the store lock. Check for a stray process (ps aux | grep whatsapp-mcp) or another MCP client pointed at the same-storedirectory.- QR doesn't display — the terminal doesn't render half-block Unicode. Try iTerm2, Windows Terminal, or similar.
- Device limit reached — WhatsApp caps linked devices. Remove one from Settings → Linked Devices on your phone.
- No messages loading — after initial auth, it can take several minutes for history to backfill. Use
request_syncto target a specific chat. - WhatsApp out of sync — delete both database files (
store/messages.dbandstore/whatsapp.db) and re-runlogin. ffmpeg not found—send_audio_messageneeds ffmpeg onPATHto convert non-Opus audio. Usesend_filefor raw audio instead.
Debug logging
By default, stderr logs redact JIDs to …<last-4>, summarise bodies as [<length>B: text|url|command] and collapse media URLs to <scheme>://<host>/…. To see content while debugging, run ./bin/whatsapp-mcp -debug serve or set WHATSAPP_MCP_DEBUG=1. Even then, phone-number-shaped digits show only their last 5 (+15551234567 → ****34567). That is obfuscation, not anonymisation: someone who knows your contacts can still correlate them.
For Claude Desktop integration issues, see the MCP documentation.
Contributing
Contributions welcome via pull request. See CONTRIBUTING.md.
Licence
MIT Licence — see LICENSE.