Discover MCPs & agents
Loading MCPs and agents…
Loading MCPs and agents…
WhatsApp MCP server - Connect Claude to WhatsApp for reading and sending messages
From the repo.
A Model Context Protocol (MCP) server for WhatsApp, enabling Claude to read and send WhatsApp messages.
Originally created by Luke Harries. Maintained by Very Good Plugins.
Watch the WhatsApp MCP demo video
Product demo generated with Remotion using simulated data.
sender_display format ("Name (phone)")Clone the repository
git clone https://github.com/verygoodplugins/whatsapp-mcp.git
cd whatsapp-mcp
Start the WhatsApp bridge
cd whatsapp-bridge
go run .
On first start, the bridge prints and stores a local REST API token at
whatsapp-bridge/store/.bridge-token. Scan the QR code with WhatsApp on
your phone to authenticate.
Configure Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"whatsapp": {
"command": "uv",
"args": [
"--directory",
"/path/to/whatsapp-mcp/whatsapp-mcp-server",
"run",
"main.py"
]
}
}
}
Replace /path/to/whatsapp-mcp with your actual path.
Restart Claude Desktop
Pull the latest changes, then refresh whichever components moved:
git pull
| You changed | What to do |
|---|---|
Bridge code (whatsapp-bridge/*.go) and you run go run . | Nothing — go run recompiles each launch. Just restart the bridge. |
| Bridge code and you run a built binary | cd whatsapp-bridge && go build -o whatsapp-bridge && ./whatsapp-bridge |
MCP server (whatsapp-mcp-server/*.py, pyproject.toml, uv.lock) | Restart Claude Desktop / Cursor — uv re-resolves from the lockfile on next launch. Force a sync with cd whatsapp-mcp-server && uv sync if needed. |
Updates do not require re-pairing or deleting whatsapp.db — your session and message history are preserved. Re-pairing is only needed when explicitly requesting full history (see Requesting full history).
For v0.2.1 and later, restart both the bridge and MCP server after updating
so the MCP server can read the bridge token. If the two components do not share
the same checkout, set the same WHATSAPP_BRIDGE_TOKEN value in both
environments.
Add to your Cursor MCP settings (~/.cursor/mcp.json):
{
"mcp": {
"servers": {
"whatsapp": {
"command": "uv",
"args": [
"--directory",
"/path/to/whatsapp-mcp/whatsapp-mcp-server",
"run",
"main.py"
]
}
}
}
}
Messages include sender_display showing "Name (phone)" format for easy identification by agents.
search_contactsSearch contacts by name or phone number.
Parameters:
query (required): Name or phone number to searchNatural Language Examples:
get_contactResolve a WhatsApp contact name from a phone number, LID, or full JID.
Parameters:
identifier (required): Phone number, LID, or full JID (aliases: phone_number, phone)
12025551234, 184125298348272, 12025551234@s.whatsapp.net, 184125298348272@lidNatural Language Examples:
list_messagesGet messages with filters, date ranges, and sorting.
Parameters:
chat_jid (optional): Filter by specific chat JIDlimit (optional): Number of messages (default 50, max 500)before_date (optional): Messages before this date (YYYY-MM-DD)after_date (optional): Messages after this date (YYYY-MM-DD)sort_by (optional): "newest" or "oldest" (default "newest")Natural Language Examples:
send_messageSend a text message to a contact or group, optionally as a quoted reply.
Parameters:
recipient (required): Phone number or group JIDmessage (required): Text content to sendquoted_message_id (optional): ID of the message to reply to. When provided, the sent message appears as a quoted reply in WhatsApp.quoted_sender_jid (optional): Full JID of the author of the quoted message. Required for group replies so WhatsApp renders the correct attribution header.quoted_content (optional): Text content of the quoted message, used for the reply preview. Only plain text is supported.mentions (optional): List of users to @-mention, as phone numbers with country code (e.g. ["12025551234"]) or JIDs. For each entry the message text must contain a matching @<number> token (e.g. "thanks @12025551234!"), which recipients' devices render as a highlighted, tappable mention that also notifies the user. Only meaningful in group chats.Inbound quoted replies are stored automatically. The quoted_message_id field in each message returned by list_messages indicates which message it is replying to (or null for non-replies).
Natural Language Examples:
mark_messages_readMark one or more messages from the same chat and sender as read. This explicitly sends WhatsApp read receipts; reading or searching messages never does so automatically.
Parameters:
message_ids (required): IDs of messages from the same chat and senderchat_jid (required): JID of the chat containing the messagessender_jid (required for groups): Full JID or bare phone number of the original message sendertimestamp (optional): RFC 3339 read timestamp; defaults to the current timeNatural Language Examples:
send_reactionSend (or remove) an emoji reaction to a message.
Parameters:
recipient (required): Chat JID the message belongs to (phone JID or group JID)message_id (required): ID of the message to react toemoji (required): Reaction emoji (e.g. "👍"). Pass an empty string "" to remove an existing reaction.from_me (optional, default false): Whether the original message was sent by the current usersender_jid (optional): Full JID of the original message sender — required for group messages when from_me is false so the correct WhatsApp key is builtInbound reactions received from others are stored automatically as messages with media_type = "reaction". The reaction_to_message_id field in each reaction message indicates which message was reacted to.
When webhook forwarding is enabled, inbound reactions are also posted to WEBHOOK_URL as typed events. Reaction removals use an empty content/reactionEmoji and reactionRemoved: true.
{
"eventType": "reaction",
"sender": "15551234567",
"chatJID": "15551234567@s.whatsapp.net",
"isFromMe": true,
"content": "👍",
"messageId": "reaction-stanza-id",
"mediaType": "reaction",
"reactionToMessageId": "target-message-id",
"reactionEmoji": "👍",
"reactionRemoved": false
}
Natural Language Examples:
send_fileSend a media file (image, video, document).
Successfully sent attachments retain their download metadata in local history.
Use their message ID and chat JID with download_media to retrieve the uploaded
bytes again while WhatsApp still serves the attachment. This also applies to
voice messages sent with send_audio_message. It does not backfill metadata for
attachments sent by older bridge versions or prevent WhatsApp media expiry.
Parameters:
recipient (required): Phone number or group JIDfile_path (required): Path to the filecaption (optional): Caption for the mediaThe bridge only reads files inside configured media roots. By default this is
~/.local/share/whatsapp-mcp/outbox; set WHATSAPP_MEDIA_ROOTS to allow
additional absolute directories.
For documents, recipients receive only the filename portion of file_path;
parent directories are not exposed.
send_audio_messageSend a voice message (automatically converts to Opus .ogg format).
Parameters:
recipient (required): Phone number or group JIDfile_path (required): Path to audio fileConverted audio is sent through the same media-path confinement as
send_file.
download_mediaDownload media from a received message. Returns the local file path, which
only helps a client that can read the filesystem — use view_media otherwise.
Parameters:
message_id (required): ID of the message with mediachat_jid (required): JID of the chat containing the messageview_mediaView the media of a message as an image, for clients with no filesystem access (Claude Desktop, a claude.ai chat). Images are returned as image content; a video returns its first frame as a still. Both are downscaled first so one photo cannot flood the context. Audio is rejected with a pointer to its transcript.
Uses FFmpeg when available (already an optional dependency); without it, images below 4 MB are returned unchanged and anything else reports what is missing.
Parameters:
message_id (required): ID of the message with mediachat_jid (required): JID of the chat containing the messagemax_dimension (optional): longest edge in pixels, default 1024max_dimension must be an integer from 1 to 2048. Invalid values are
rejected before the server downloads or renders any media.
transcribe_audioTranscribe a voice note with whisper.cpp (the default) or an OpenAI-compatible
endpoint and return its text. The transcript is also written into the message's
empty content field, so afterwards it is readable through list_messages
by any client — including one with no filesystem access — without transcribing
again. whisper.cpp runs entirely on this machine; the HTTP provider sends audio
to the endpoint you configure. Use a loopback URL to keep transcription local.
A stored transcript is returned immediately; a fresh one took about 2 s for a
30-second note with large-v3-turbo on an M-series Mac. Transcripts are
prefixed with [transcript (whisper <model>)] or
[transcript (openai_compatible <model>)] so they cannot be mistaken for
text a human typed, and a real message is never overwritten.
Requirements: whisper.cpp
(whisper-cli on PATH), FFmpeg, and WHISPER_MODEL pointing at a model
file. Optionally WHISPER_LANGUAGE (default auto).
To reuse an existing service, such as a local Parakeet server, configure:
WHATSAPP_TRANSCRIPTION_PROVIDER=openai_compatible
WHATSAPP_TRANSCRIPTION_URL=http://127.0.0.1:8178/v1/audio/transcriptions
WHATSAPP_TRANSCRIPTION_MODEL=parakeet
WHATSAPP_TRANSCRIPTION_PROVIDER defaults to whisper_cpp. For
openai_compatible, URL and MODEL are required. URL is the full endpoint;
no path is appended. Optional WHATSAPP_TRANSCRIPTION_API_KEY supplies a bearer
token, and WHATSAPP_TRANSCRIPTION_LANGUAGE supplies a language code (auto
by default, omitted from the HTTP request). This provider uploads the original
audio using multipart file, model, and response_format=json; the server
must decode it (including WhatsApp Opus/OGG) and return {"text": "..."}.
It requires no local whisper.cpp, model file, or FFmpeg. Remote URLs send audio
off the machine; redirects, environment proxies, .netrc credentials, and
automatic provider fallbacks are disabled.
HTTP connections time out after 10 seconds, HTTP reads and Whisper inference
after 300 seconds, and local FFmpeg decoding after 60 seconds.
Stored transcripts are reused across provider changes unless force=true.
Cache reads and writes use both message ID and chat JID.
Parameters:
message_id (required): ID of the message with the voice notechat_jid (required): JID of the chat containing the messageforce (optional): transcribe again even when a transcript is storedAll chat tools (list_chats, get_chat, get_direct_chat_by_contact,
get_contact_chats) return the same chat shape:
{
"jid": "1234567890@s.whatsapp.net",
"name": "Alice",
"is_group": false,
"last_message_time": "2024-01-15T10:30:00+00:00",
"last_message": "hello world", // null when include_last_message=false
"last_sender": "1234567890", // null when include_last_message=false
"last_is_from_me": false,
"last_read_time": "2024-01-15T09:00:00+00:00", // how far the chat is read
"unread": true // last message is inbound and unread
}
last_read_time / unread)last_read_time is the bridge's read marker for the chat, fed by read
receipts from your own devices and backfilled from history sync. unread is
derived from it: true when the chat's last message is inbound and newer than
the marker. This distinguishes a genuinely unread chat from one whose last
message merely happens to be inbound but was already read on the phone.
Caveats:
unread then falls back to the old heuristic and reports
true. Stores written by a bridge older than the chats.last_read_time
column report last_read_time: null and behave the same way.unread is a chat-level flag, not an unread count. WhatsApp's unread
counter is not persisted.list_chatsList all chats with metadata.
Parameters:
limit (optional): Number of chats (default 50, max 200)get_chatGet specific chat metadata by JID.
Parameters:
jid (required): Chat JIDget_direct_chat_by_contactFind a direct message chat with a contact.
Parameters:
phone (required): Phone number of the contactget_contact_chatsList all chats involving a specific contact.
Parameters:
phone (required): Phone number of the contactget_last_interactionGet the last message exchanged with a contact.
Parameters:
phone (required): Phone number of the contactget_message_contextGet messages around a specific message for context.
Parameters:
message_id (required): ID of the target messagechat_jid (required): JID of the chatbefore (optional): Number of messages before (default 5)after (optional): Number of messages after (default 5)Copy .env.example to .env and configure as needed:
| Variable | Default | Description |
|---|---|---|
WHATSAPP_BRIDGE_PORT | 8080 | Port for Go bridge REST API |
WEBHOOK_URL | http://localhost:8769/whatsapp/webhook | Webhook for incoming messages |
WEBHOOK_ENABLED | true | Set to false to disable outbound webhooks |
WHATSAPP_AUTO_DOWNLOAD_MEDIA | true | Automatically download incoming media, including webhook image bytes. false keeps webhook metadata/text without mediaBase64 and leaves downloads to /api/download (download_media); delayed downloads may fail after media expires. Status messages are stored but never auto-downloaded or forwarded |
FORWARD_SELF | true | Forward messages sent by self |
WHATSAPP_DB_PATH | ../whatsapp-bridge/store/messages.db | Path to SQLite database |
WHATSMEOW_DB_PATH | ../whatsapp-bridge/store/whatsapp.db | whatsmeow DB used for LID ↔ phone resolution |
WHATSAPP_API_URL | http://localhost:8080/api | Go bridge REST API URL |
WHATSAPP_BRIDGE_TOKEN | generated next to WHATSMEOW_DB_PATH as .bridge-token | Bearer token for bridge REST calls; also signed onto outbound webhook POSTs |
WHATSAPP_MEDIA_ROOTS | ~/.local/share/whatsapp-mcp/outbox | Path-list of directories allowed for outbound media files |
WHATSAPP_DEVICE_NAME | whatsmeow (whatsmeow default) | Label shown for this connection under WhatsApp > Linked Devices. Set to a recognisable name. Applies at pair time only (re-pair to change) |
WHATSAPP_MCP_TRANSPORT | stdio | MCP transport to serve clients: stdio, http, or sse |
WHATSAPP_MCP_HOST | 127.0.0.1 | Bind address for the http/sse transports |
WHATSAPP_MCP_PORT | 8000 | Port for the http/sse transports |
WHATSAPP_PARENT_WATCHDOG_S | 30 | Stdio parent-liveness poll interval (seconds); exits on parent reparent only |
By default the server speaks MCP over stdio, which is what local clients
like Claude Desktop and Cursor launch. To serve the server over the network
instead, set WHATSAPP_MCP_TRANSPORT:
# Streamable HTTP (current spec transport for remote MCP), endpoint at /mcp
WHATSAPP_MCP_TRANSPORT=http WHATSAPP_MCP_PORT=8000 uv run main.py
# Legacy Server-Sent Events transport (deprecated in the MCP spec), endpoint at /sse
WHATSAPP_MCP_TRANSPORT=sse uv run main.py
http is an alias for the spec's streamable-http transport and is the
recommended choice for remote connections; sse is kept for older clients.
Security:
WHATSAPP_MCP_HOSTdefaults to127.0.0.1, so the HTTP/SSE server is reachable only from the local machine. The server has no built-in authentication, and the underlying bridge can read and send WhatsApp messages on your account. Only bind to a non-loopback address (e.g.0.0.0.0) if you place an authenticating reverse proxy or tunnel in front of it.
The bridge requires bearer-token authentication for every /api/* request and
accepts only exact loopback Host headers for its configured port. This protects
the local REST API from other local processes and browser DNS-rebinding attacks.
On first start, the bridge generates a 256-bit token, writes it to
.bridge-token in the active bridge store directory with owner-only
permissions, and prints a setup banner. The MCP server reads
WHATSAPP_BRIDGE_TOKEN first, then falls back to .bridge-token in the same
directory as WHATSMEOW_DB_PATH. For split deployments, containers, or process
managers that do not share the store directory, set the same
WHATSAPP_BRIDGE_TOKEN value for both the bridge and MCP server.
The bridge also signs its outbound webhook POSTs (to WEBHOOK_URL) with this
same token, sent as an X-Bridge-Token: <token> header — a dedicated header
rather than Authorization, so it never collides with a receiver's own
Authorization-based auth (e.g. HTTP Basic auth embedded in WEBHOOK_URL as
http://user:pass@host/..., which net/http applies automatically as long as
the bridge doesn't set its own Authorization header). The header is attached only when a token is configured and WEBHOOK_URL was
explicitly set — never to the built-in local default. The bridge token also
authorizes /api/* calls like sending messages, and nothing has vetted the
implicit default address, so it must never be handed to whatever process
happens to be listening there. Upgrades that predate the token rollout, or
that never set WEBHOOK_URL, keep working unchanged. The webhook client also
never follows redirects, so a misconfigured or malicious endpoint can't
redirect the bridge into leaking the token to a different host. If your
webhook receiver enforces the token, set its copy to this exact value: e.g.
the AutoHub hub's WHATSAPP_BRIDGE_TOKEN must equal this bridge's token (from
.bridge-token or its own env) — the hub accepts it via X-Bridge-Token or
Authorization: Bearer. The bridge always sends the token it has; the hub
rejects unauthenticated forwards only once its WHATSAPP_BRIDGE_TOKEN is set
to the matching value.
Outbound media_path values are confined to WHATSAPP_MEDIA_ROOTS. The default
outbox is ~/.local/share/whatsapp-mcp/outbox, created on bridge startup. Move
files there before calling send_file or send_audio_message, or set
WHATSAPP_MEDIA_ROOTS to a colon-separated list of absolute directories.
The bridge keeps its runtime state in store/, resolved relative to its
working directory. WHATSAPP_DB_PATH and WHATSMEOW_DB_PATH configure the
MCP server's reads; they do not change the bridge's store location. That
store contains:
| Path | Contents |
|---|---|
whatsapp.db | whatsmeow session state, including linked-device credentials |
messages.db | locally synced chat and message history |
<chat_jid>/ | downloaded images, voice notes, documents, and other media |
.bridge-token | the generated REST API bearer token, unless supplied through the environment |
The application does not encrypt these files at rest. Anyone who can read
whatsapp.db can obtain the linked-device credentials. Moving the store does
not automatically tighten permissions on existing files; check the destination's
access permissions as part of the move. An encrypted volume adds protection
when the machine or a backup is lost.
Cloud-synced folders: a checkout inside Google Drive, Dropbox, iCloud Drive, or OneDrive puts the default store within that service's sync scope. Keep the checkout or its runtime store outside synced folders. Relocating prevents future sync of that store; it does not remove copies or version history already uploaded to a provider.
Stop the bridge and every MCP server before copying or moving any files. Quit clients that launch the stdio server (such as Claude Desktop or Cursor), stop any standalone HTTP/SSE MCP server, and disable automatic restarts while migrating. The macOS jobs only manage the bridge and its monitor, so stopping them does not stop MCP clients. If installed, unload both jobs:
launchctl bootout "gui/$(id -u)/com.whatsapp-mcp.bridge-monitor"
launchctl bootout "gui/$(id -u)/com.whatsapp-mcp.bridge"
For a manually started bridge, stop it in its terminal. Confirm all bridge and MCP server processes have exited. Never copy live SQLite databases.
Build the binary and move the entire existing store, including hidden
files and any SQLite -wal, -shm, or journal files, to an unsynced directory.
Replace the checkout path below, and use the same terminal for later examples.
If you already run the bridge from another working directory, use that
directory's store/ as the source instead.
repo_dir="/absolute/path/to/whatsapp-mcp"
runtime_dir="$HOME/.local/share/whatsapp-mcp/runtime"
(
set -eu
cd "$repo_dir/whatsapp-bridge"
go build -o whatsapp-bridge .
mkdir -p "$runtime_dir"
chmod 700 "$runtime_dir"
if [ -e "$runtime_dir/store" ] || [ -L "$runtime_dir/store" ]; then
echo "Destination store already exists; stop and inspect it before migrating." >&2
exit 1
fi
mv "$repo_dir/whatsapp-bridge/store" "$runtime_dir/store"
)
Continue only if the move succeeds. Do not merge two stores or start with an empty store to relocate an existing session: that creates a new session and loses access to the existing local history.
In every MCP client or server configuration, set both database paths to
the moved files, using absolute paths (JSON does not expand $HOME or ~):
"env": {
"WHATSAPP_DB_PATH": "/Users/you/.local/share/whatsapp-mcp/runtime/store/messages.db",
"WHATSMEOW_DB_PATH": "/Users/you/.local/share/whatsapp-mcp/runtime/store/whatsapp.db"
}
The MCP server reads .bridge-token beside WHATSMEOW_DB_PATH when
WHATSAPP_BRIDGE_TOKEN is unset. An explicit token takes precedence: preserve
the same value in the bridge, MCP clients, and any authenticated webhook
receiver. Keep token values private.
Choose how to restart the bridge, then restart the MCP servers and clients with their updated configuration:
Manual: launch the built binary from the new runtime directory:
cd "$runtime_dir"
"$repo_dir/whatsapp-bridge/whatsapp-bridge"
macOS launchd: before reloading either job, edit
~/Library/Application Support/whatsapp-mcp/launchd.env to set
WHATSAPP_BRIDGE_DIR to the absolute runtime directory. Keep
WHATSAPP_BRIDGE_BINARY pointing to the built binary in the checkout.
The runner explicitly executes cd "$WHATSAPP_BRIDGE_DIR"; changing only
the plist's WorkingDirectory does not relocate the store. Update that
plist value too so both directory settings agree:
/usr/libexec/PlistBuddy -c "Set :WorkingDirectory $runtime_dir" \
"$HOME/Library/LaunchAgents/com.whatsapp-mcp.bridge.plist"
Recent installers also capture WHATSAPP_BRIDGE_TOKEN in launchd.env,
which both the runner and monitor source. For a generated file token, ensure
that cached value matches the moved store/.bridge-token; update a stale
cached value privately before restarting. For an explicitly configured
token, retain the same override in all consumers. Changing
WHATSAPP_BRIDGE_DIR alone does not update the cached token. Keep
launchd.env owner-readable/writable only (chmod 600).
launchctl bootstrap "gui/$(id -u)" \
"$HOME/Library/LaunchAgents/com.whatsapp-mcp.bridge.plist"
launchctl bootstrap "gui/$(id -u)" \
"$HOME/Library/LaunchAgents/com.whatsapp-mcp.bridge-monitor.plist"
Check that the bridge reconnects with the existing session and that the MCP client can read known history. An unexpected QR pairing prompt or empty history is a reason to stop and recheck paths before proceeding.
Installer caveat: rerunning scripts/install-launchd-macos.sh rewrites
launchd.env and both plists, restores the checkout's bridge directory, and
starts the jobs immediately. It does not preserve this custom runtime location.
After a reinstall, stop both jobs again and reapply the directory and token
settings above before restarting them. Review any newly created checkout store;
do not replace the relocated store with it.
Fresh manual installation: only when there is no session or history to
preserve, omit the mv step, create the runtime directory, and launch the built
binary there. Pair the new device, then point the MCP server at the new databases
and token as above. This is separate from migrating an existing installation.
macOS users can install optional per-user launchd jobs that start the Go
bridge at login and monitor it every 60 seconds for API health, disconnects, and
QR relink signals. The installer does not require sudo and does not install or
start the MCP server.
scripts/install-launchd-macos.sh
The installer builds whatsapp-bridge/whatsapp-bridge with go build when Go is
available, writes generated support files to
~/Library/Application Support/whatsapp-mcp/, writes LaunchAgents to
~/Library/LaunchAgents/, and writes logs to ~/Library/Logs/whatsapp-mcp/.
It safely reloads only these labels:
com.whatsapp-mcp.bridgecom.whatsapp-mcp.bridge-monitorTo customize the launchd environment, export values before running the installer. Re-run the installer after changing them.
export WHATSAPP_BRIDGE_PORT=8080
export WEBHOOK_URL=http://localhost:8769/whatsapp/webhook
export FORWARD_SELF=false
export WHATSAPP_MEDIA_ROOTS="$HOME/.local/share/whatsapp-mcp/outbox"
scripts/install-launchd-macos.sh
Verify the jobs and inspect logs:
launchctl print gui/$(id -u)/com.whatsapp-mcp.bridge
launchctl print gui/$(id -u)/com.whatsapp-mcp.bridge-monitor
tail -n 100 ~/Library/Logs/whatsapp-mcp/bridge.err.log
tail -n 100 ~/Library/Logs/whatsapp-mcp/monitor.err.log
The monitor sends a macOS notification once per failure type until recovery. It alerts when the bridge LaunchAgent is unloaded, the token is missing, the health endpoint is unreachable, WhatsApp is disconnected, or recent logs indicate that QR relinking is needed.
If store/.bridge-token lives inside a macOS TCC-protected location (for example
~/Documents or ~/Desktop), the sandboxed monitor can be denied read access to
it. The installer avoids this by copying the resolved token into the mode-600
launchd.env so the monitor reads it from the environment; if you ever see the
monitor exit without alerting, re-run the installer, or set WHATSAPP_BRIDGE_TOKEN
explicitly before running it.
Uninstall the generated LaunchAgents and support files with:
scripts/uninstall-launchd-macos.sh
Uninstall preserves whatsapp-bridge/store/, including WhatsApp session DBs,
message DBs, media, and .bridge-token. Logs are left in
~/Library/Logs/whatsapp-mcp/ for manual cleanup.
| Flag | Default | Description |
|---|---|---|
--full-history-pair | false | Request full history at pair time. Only takes effect on a fresh pair (no existing whatsapp.db); no-op for already-paired sessions. The phone ultimately decides the actual history window sent — see Requesting full history below. |
whatsmeow's default pairing asks for "recent sync" — roughly the last 3 months, with the exact window decided by the phone. If you want to pull more history at pair time:
# Stop the bridge
launchctl bootout gui/$UID/com.whatsapp-mcp.bridge # or however you manage it
# Back up, then remove the auth session (keeps messages.db intact)
cp whatsapp-bridge/store/whatsapp.db{,.bak}
rm whatsapp-bridge/store/whatsapp.db
# Re-pair with the flag
cd whatsapp-bridge
./whatsapp-bridge --full-history-pair
# Scan the QR with WhatsApp → Settings → Linked Devices → Link a Device
# Wait for "History sync complete" in the logs (can take 10-30 minutes)
# Ctrl+C when sync has quiesced, then restart under your normal process manager
Caveats:
whatsapp.db already present, no new pair handshake fires and the flag is a no-op.--full-history-pair only applies to a fresh pair, so recovering a gap in one
chat otherwise means deleting whatsapp.db and re-syncing everything. To ask
the phone for older messages in a single chat without re-pairing:
curl -X POST http://127.0.0.1:8080/api/history \
-H "Authorization: Bearer $(cat whatsapp-bridge/store/.bridge-token)" \
-H "Content-Type: application/json" \
-d '{"chat_jid": "1234567890@s.whatsapp.net", "count": 50}'
The request is anchored on the oldest message already stored for that chat,
so the phone returns messages from before it. Call it repeatedly to page
further back. Results arrive asynchronously through the normal history-sync
handler and land in messages.db — typically within a few seconds.
| Field | Required | Description |
|---|---|---|
chat_jid | yes | Chat to backfill (...@s.whatsapp.net or ...@g.us) |
count | no | Messages to request; default 50, capped at 500 |
Caveats:
count is a request rather than a guarantee.404; send or receive one
message first.The bridge captures incoming WhatsApp voice and video calls live into a
dedicated calls table in messages.db. When a 1:1 call arrives
(CallOffer) or a group call is announced (CallOfferNotice), a row is
inserted with result='in_progress'. Subsequent CallAccept /
CallReject / CallTerminate events update the row — final result becomes
answered, rejected, missed, or ended depending on the event
sequence. See the state-machine comment above StoreCallOffer in main.go
for the exact transitions.
CREATE TABLE calls (
call_id TEXT,
chat_jid TEXT, -- group JID for group calls, call creator JID for 1:1
from_jid TEXT, -- JID of whoever started the call
timestamp TIMESTAMP, -- call start time
is_from_me BOOLEAN,
call_type TEXT, -- 'voice' or 'video'
is_group BOOLEAN,
result TEXT, -- 'in_progress' | 'answered' | 'ended' |
-- 'missed' | 'rejected'
duration_sec INTEGER, -- computed when the call terminates
ended_at TIMESTAMP,
reason TEXT, -- terminate reason string from whatsmeow
PRIMARY KEY (call_id, chat_jid)
);
call_type='voice'. CallOffer events don't
expose media type directly (it's buried in the binary call data). Group
calls via CallOfferNotice include a Media field and are recorded
accurately as voice or video.flowchart TB
subgraph Clients["AI Clients"]
CD[Claude Desktop]
CU[Cursor IDE]
CC[Claude Code]
end
subgraph MCP["MCP Layer"]
PY[Python MCP Server<br/>FastMCP]
end
subgraph Bridge["WhatsApp Bridge"]
GO[Go Bridge<br/>whatsmeow]
DB[(SQLite<br/>messages.db)]
WH[Webhook Handler]
end
subgraph External["External Services"]
WA[WhatsApp Web API]
EXT[External Webhook<br/>Receiver]
end
CD & CU & CC -->|MCP Protocol| PY
PY -->|REST API| GO
PY -->|Read| DB
GO -->|Store| DB
GO <-->|WebSocket| WA
GO -->|Forward Messages| WH
WH -->|POST| EXT
flowchart LR
subgraph GoAPI["Go Bridge REST API"]
direction TB
SEND["/api/send"]
READ["/api/mark-read"]
DOWN["/api/download"]
REACT["/api/react"]
TYPE["/api/typing"]
HIST["/api/history"]
HEALTH["/api/health"]
end
subgraph MCPTools["MCP Tools (15 total)"]
direction TB
CONT["Contact Tools<br/>search_contacts, get_contact"]
MSG["Message Tools<br/>list_messages, send_message, etc."]
CHAT["Chat Tools<br/>list_chats, get_chat, etc."]
MEDIA["Media Tools<br/>send_file, download_media, etc."]
end
MCPTools -->|HTTP Requests| GoAPI
sequenceDiagram
participant User as User
participant Claude as Claude Desktop
participant MCP as Python MCP Server
participant Bridge as Go Bridge
participant WA as WhatsApp
User->>Claude: "Send 'Hello' to Mom"
Claude->>MCP: send_message(recipient, message)
MCP->>Bridge: POST /api/send
Bridge->>WA: Send via WebSocket
WA-->>Bridge: Delivery confirmation
Bridge-->>MCP: Success response
MCP-->>Claude: Message sent
Claude-->>User: "Message sent to Mom"
sequenceDiagram
participant WA as WhatsApp
participant Bridge as Go Bridge
participant DB as SQLite
participant WH as Webhook
participant EXT as External Service
WA->>Bridge: New message
Bridge->>DB: Store message
Bridge->>Bridge: Auto-download media
Bridge->>WH: Forward to webhook
WH->>EXT: POST with message data
Note over EXT: Process incoming message
cd whatsapp-mcp-server
uv pip install -e ".[dev]"
uv run pytest -v
# Python
cd whatsapp-mcp-server
uv run ruff check .
uv run ruff format .
# Go
cd whatsapp-bridge
golangci-lint run
# Go bridge
cd whatsapp-bridge
go build -o whatsapp-bridge
# Run the binary
./whatsapp-bridge
# During development (avoids stale binaries)
go run .
Releases use Release Please automation; maintainer steps and fallback procedures are documented in docs/RELEASING.md.
Client outdated or HTTP 405: Update to the latest
release and rebuild the bridge. WhatsApp periodically raises the minimum
supported linked-device client version, which can make older whatsmeow builds
fail before pairing completes.companion_reg_refresh notification, whatsmeow rotates the pairing
secret, and the bridge prints a new QR code marked QR code refreshed.
Scan that one — the earlier code is dead at that point. Needs a whatsmeow
build from 2026-09-15 or later; older ones never emit the rotated code.whatsapp-bridge/store, then move
whatsapp-bridge/store/whatsapp.db aside and re-authenticate. Keep
messages.db unless you intentionally want to discard local message history..bridge-token next to WHATSMEOW_DB_PATH, then restart the MCP server. If
the MCP server cannot read that file, set WHATSAPP_BRIDGE_TOKEN to the same
value in both environments.WHATSAPP_API_URL with
http://127.0.0.1:<port>/api, http://localhost:<port>/api, or
http://[::1]:<port>/api; custom hostnames and missing ports are rejected.~/.local/share/whatsapp-mcp/outbox or add its absolute parent directory to
WHATSAPP_MEDIA_ROOTS.Some WhatsApp account state is managed by whatsmeow in
whatsapp-bridge/store/whatsapp.db. If the bridge reports errors like:
SendAppState failed: server returned error updating app state (regular_low):
<error code="409" text="conflict"/>
failed to verify patch v12345: mismatching LTHash
then WhatsApp's app-state patch chain for the linked device is out of sync.
This usually affects operations that write chat settings such as archive,
mute, or pin state. Incoming and outgoing messages may still work because
message storage lives separately in messages.db.
Known manual resync attempts such as FetchAppState(..., fullSync=true) may
still fail on this upstream app-state error class. The practical recovery path
is to reset the whatsmeow session and re-pair:
# Stop the bridge first.
launchctl bootout gui/$UID/com.whatsapp-mcp.bridge # or however you manage it
# Back up the whole runtime store.
cp -a whatsapp-bridge/store whatsapp-bridge/store.bak.$(date +%Y%m%d%H%M%S)
# Reset only the whatsmeow session/app-state DB.
mv whatsapp-bridge/store/whatsapp.db whatsapp-bridge/store/whatsapp.db.lthash.bak
# Restart the bridge and scan the new QR code.
cd whatsapp-bridge
./whatsapp-bridge # or `go run .` during development
Do not remove whatsapp-bridge/store/messages.db for this recovery unless you
also want to delete the local message archive.
Windows requires CGO for go-sqlite3. Install MSYS2 and enable CGO:
go env -w CGO_ENABLED=1
go run .
Caution: As with many MCP servers, this is subject to the lethal trifecta. Prompt injection could lead to private data exfiltration. Use with awareness.
On Unix-like systems, newly created bridge store/ and per-chat media
directories request owner-only permissions (0700), and newly downloaded media
files request 0600. This is local filesystem defense-in-depth; it does not
encrypt data or protect it from privileged users, backups, or sync services.
Existing directories and files retain their permissions: the bridge does not
recursively change an existing store tree.
MIT License - see LICENSE for details.
This project is a maintained fork of lharries/whatsapp-mcp, originally created by Luke Harries.
Why we forked: The original repository hasn't been updated since April 2025. We needed continued maintenance, bug fixes, and new features for production use.
Highlights since the fork:
/api/typing, /api/health, and webhook forwarding (with reply context + image media)get_contact tool, sender_display field, and LID ↔ phone resolution via the whatsmeow storecalls table--full-history-pair flag to request extended history at pair timeStreamReplaced session conflicts; pinned anyio to dodge a cancel-scope regressionThe full release-by-release list lives in CHANGELOG.md.
Recent contributors (huge thanks):
StreamReplaced recovery (#27)anyio cancel-scope pin (#44)And to Luke for creating the original project. See CONTRIBUTING.md if you'd like to join in.
Replace {MCP_ENDPOINT_URL} with this MCP’s endpoint URL (from its repo or docs above). No API key — you connect directly.
Tool
OS
Config file: ~/.cursor/mcp.json
{
"mcpServers": {
"mcp-server": {
"url": "{MCP_ENDPOINT_URL}"
}
}
}Paste into mcpServers in the config file. Restart Cursor after saving.
If this MCP is also published on mcpchannel.ai, you can subscribe from Browse and use the gateway config there instead.