mcp-whatsapp
WhatsApp MCP Server
A single-binary Go MCP server that wraps whatsmeow to expose a personal WhatsApp account to LLMs. Your MCP client (Claude Desktop, Cursor, etc.) launches whatsapp-mcp serve over stdio on demand — no background daemon, no two-process bridge. Messages are cached in local SQLite and only travel to the model when the agent calls a tool.
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 — 41 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.25+ (build-time only; runtime needs just the compiled binary).
An MCP client that speaks stdio (Claude Desktop, Cursor, 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-mcpPair your phone (first run only)
./bin/whatsapp-mcp loginScan the QR code with WhatsApp on your phone (Settings → Linked Devices → Link a Device). The pairing persists to ./store/whatsapp.db. Re-run login only when WhatsApp invalidates the session or you want to switch accounts.
Connect your MCP client
Add this to your MCP client config, replacing {{PATH_TO_REPO}} with the absolute path to your clone:
{
"mcpServers": {
"whatsapp": {
"command": "{{PATH_TO_REPO}}/bin/whatsapp-mcp",
"args": ["-store", "{{PATH_TO_REPO}}/store", "serve"]
}
}
}Config file locations:
Claude Desktop:
~/Library/Application Support/Claude/claude_desktop_config.jsonCursor:
~/.cursor/mcp.json
Restart the client. WhatsApp will appear as an available integration; the client starts whatsapp-mcp serve automatically when it needs tools and terminates it when the session ends.
Architecture
One binary, five internal packages:
cmd/whatsapp-mcp/ login / serve / smoke subcommands
internal/client/ whatsmeow client wrapper (send, download, events, history, features)
internal/store/ SQLite cache, LID resolution, query layer
internal/media/ ogg parsing, waveform synthesis, ffmpeg shell-out
internal/mcp/ mark3labs/mcp-go server + tool registrationsProcess lifecycle
serve starts when the MCP client needs it and exits when the client disconnects. A flock(2) on store/.lock prevents two instances racing on the same store (WhatsApp would kick one of the two linked-device connections anyway).
The trade-off: events are persisted to SQLite only while serve is running. When the MCP client quits, the WhatsApp connection closes. On the next launch, whatsmeow emits events.HistorySync events that backfill conversations into SQLite, but the recovery window is governed by WhatsApp's server-side retention for multidevice clients — not by this codebase. Messages that arrive during a gap long enough to outlast WhatsApp's retention are not recoverable. For shorter, known gaps, the request_sync tool triggers a per-chat backfill on demand.
Data storage
Everything lives under ./store/ (override with -store DIR):
store/messages.db— local chat/message cache, indexed for search.store/whatsapp.db— whatsmeow's own device/session state.store/.lock— ephemeral advisory lock for single-instanceserve.
Data flow
The client sends a JSON-RPC
tools/calltoserveover stdio.The MCP layer dispatches to an internal handler.
The handler either queries the local SQLite store or calls whatsmeow directly (send, download, reactions, etc.).
Incoming WhatsApp events are persisted to the store in a background goroutine inside the same process, so query tools always see current state.
Running continuously (advanced)
By default, whatsapp-mcp serve runs on-demand — your MCP client spawns it and kills it with the session. If you need tighter message capture (e.g. you're offline from Claude for days at a time), the options below all trade something for it. Today there is no way to have both a keep-alive event tracker and functioning MCP clients at the same time: the single-instance lock (store/.lock) is exclusive, and MCP is stdio-only, so whatever process holds the lock owns the whatsmeow connection and the MCP stdio both. Pick one.
Pattern A — accept the default (recommended for most). Use request_sync to backfill known gaps after reconnecting.
Pattern B — keep-alive event tracker (advanced). Run:
./bin/whatsapp-mcp serve < <(tail -f /dev/null)tail -f /dev/null never sends EOF, so ServeStdio stays in its read loop indefinitely. The process holds the WhatsApp connection and writes events to SQLite. Caveat: no MCP client can use whatsapp-mcp while this runs — every client that tries to spawn serve will hit the lock and fail. This pattern is only useful if you read SQLite directly with another tool, or if you kill the tracker before opening Claude.
Pattern C — Claude Code SessionStart hook. If you mostly use WhatsApp from inside one specific project via Claude Code, a .claude/hooks/setup.sh + cleanup.sh pair can start the binary on session open and stop it on session close. The same lock caveat applies — if the hook is running, remove the whatsapp entry from your MCP config for that client, or accept that MCP will be unavailable while the hook process is alive.
setup.sh skeleton (~place at .claude/hooks/setup.sh in your project):
#!/bin/bash
# WhatsApp MCP SessionStart hook — macOS only (uses osascript)
if [[ "$OSTYPE" != "darwin"* ]]; then echo "macOS only (osascript)"; exit 0; fi
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
if pgrep -f "whatsapp-mcp" > /dev/null 2>&1; then
echo "whatsapp-mcp: already running"
else
echo "whatsapp-mcp: starting in Terminal..."
osascript -e "tell application \"Terminal\"
activate
do script \"$REPO_DIR/bin/whatsapp-mcp serve < <(tail -f /dev/null)\"
delay 0.5
set miniaturized of front window to true
end tell" 2>/dev/null || echo " (Could not open Terminal — run manually: $REPO_DIR/bin/whatsapp-mcp serve)"
fi
exit 0cleanup.sh skeleton (~place at .claude/hooks/cleanup.sh in your project):
#!/bin/bash
# WhatsApp MCP SessionStop hook — stop the keep-alive process
# Close Terminal windows showing the process before killing it
osascript <<'EOF' 2>/dev/null
tell application "Terminal"
set windowsToClose to {}
repeat with w in windows
repeat with t in tabs of w
try
if (name of t) contains "whatsapp-mcp" then
set end of windowsToClose to w
exit repeat
end if
end try
end repeat
end repeat
repeat with w in windowsToClose
close w saving no
end repeat
end tell
EOF
# Graceful shutdown, then force-kill if needed
pkill -f "whatsapp-mcp" 2>/dev/null
sleep 1
pkill -9 -f "whatsapp-mcp" 2>/dev/null
exit 0Why isn't there a real daemon mode?
Unifying the bridge and the MCP server into one binary keeps local install simple — one auth, one store, one lock. The cost is that you can't both background-track events and serve MCP clients from the same process. A future whatsapp-mcp sync subcommand (event-only, no ServeStdio) paired with a read-only serve mode would unlock that. It's not implemented today.
Tools
41 tools, grouped by purpose.
Read / query
Tool | Purpose |
| Substring search across cached contact names and phone numbers |
| Query + filter messages; returns formatted text with context windows |
| List chats with last-message preview; sort by activity or name |
| Chat metadata by JID |
| Before/after window around a specific message |
| Download persisted media to a local path |
| Ask WhatsApp to backfill history for a chat |
Send
Tool | Purpose |
| Send a text message to a phone number or JID |
| Send image/video/document/raw audio with optional caption; |
| Send a voice note (auto-converts via ffmpeg if not |
| Send a poll with a question and 2+ options; |
| Cast a vote on a previously-seen poll; |
| Return the tally for a poll we have cached (includes 0-vote options) |
| Send a contact card; synthesises a vCard 3.0 from |
Message actions
Tool | Purpose |
| Mark specific message IDs as read |
| Ack the most recent incoming messages in a chat to clear the unread badge |
| React to a message (empty emoji clears an existing reaction) |
| Text reply that quotes a prior message |
| Edit a previously-sent message |
| Revoke (delete for everyone) a message |
| Set per-chat composing / recording presence |
Groups
Tool | Purpose |
| Create a group with a name and initial participants |
| Leave a group |
| List all groups the user is a member of |
| Full group metadata (participants, settings, invite config) |
| Add / remove / promote / demote participants ( |
| Change the group subject |
| Change the group description; empty string clears it |
| Toggle announce-only mode (only admins can send) |
| Toggle locked mode (only admins can edit group metadata) |
| Get the invite link; |
| Join a group via a |
Blocklist
Tool | Purpose |
| Return the current blocklist |
| Block a contact by phone number or JID |
| Unblock a contact |
Privacy / presence / status
Tool | Purpose |
| Set own availability ( |
| Current privacy settings as JSON |
| Change one privacy setting by |
| Update the profile "About" text; empty string clears it |
Admin
Tool | Purpose |
| Batch-check which phone numbers are registered on WhatsApp |
| Report whether the bridge is connected and which account it's paired as |
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.
Re-authentication: WhatsApp may invalidate the linked-device session periodically; re-run
./bin/whatsapp-mcp loginwhen that happens.Message gaps when
serveisn't running: events only flow into SQLite while the binary is alive. Messages sent during an offline window are recovered on next reconnect only if WhatsApp's multidevice retention still holds them; for longer gaps userequest_syncper chat, or accept the loss.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 stdio (requires -tags=e2e)
make smoke # boot-test the server without connecting to WhatsAppUpgrading whatsmeow
Weekly CI runs an upstream upgrade probe. To do it manually:
make upgrade-checkThis 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— run./bin/whatsapp-mcp loginfirst.servecannot display a QR because its stdout is reserved for MCP JSON-RPC.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.
For Claude Desktop integration issues, see the MCP documentation.
Contributing
Contributions welcome via pull request. See CONTRIBUTING.md.
Licence
MIT Licence — see LICENSE.
Latest Blog Posts
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/Sealjay/mcp-whatsapp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server