Labsco
Sealjay logo

mcp-whatsapp

โ˜… 4

from Sealjay

Local MCP server for a personal WhatsApp account. Single Go binary wrapping whatsmeow. Adds LID resolution, sent-message storage, disappearing-message timers, targeted history sync. Personal-use; Meta ToS applies.

๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅโœ“ VerifiedFreeAdvanced setup

WhatsApp MCP Server

License: MIT CI Go Report Card Go 1.25+ MCP 42 tools whatsmeow Sealjay/mcp-whatsapp MCP server GitHub issues

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 @lid JIDs 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_sync tool.
  • 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) on store/.lock prevents two serve processes racing on the same SQLite files.

Architecture

One binary, seven internal packages:

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 endpoint
internal/mcp/           mark3labs/mcp-go 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

Process lifecycle

serve runs as a long-lived HTTP daemon. MCP clients connect and disconnect freely; the daemon stays up and continues receiving WhatsApp events. 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. If the daemon stops, the WhatsApp connection closes. On the next start, 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-instance serve.

Data flow

  1. The client sends a JSON-RPC tools/call to serve over HTTP.
  2. The MCP layer dispatches to an internal handler.
  3. The handler either queries the local SQLite store or calls whatsmeow directly (send, download, reactions, etc.).
  4. Incoming WhatsApp events are persisted to the store in a background goroutine inside the same process, so query tools always see current state.

Running the daemon

The daemon is designed to run independently of any MCP client. Three supported lifecycle models:

macOS โ€” launchd. Template at docs/launchd/com.sealjay.whatsapp-mcp.plist. Copy to ~/Library/LaunchAgents/, replace {{PATH_TO_REPO}} / {{STORE_DIR}} placeholders, launchctl load. Daemon runs from login onwards.

Linux โ€” systemd user unit. Template at docs/systemd/whatsapp-mcp.service. Copy to ~/.config/systemd/user/, replace placeholders, systemctl --user enable --now whatsapp-mcp.

Claude Code SessionStart hook. For project-scoped lifetimes, drop docs/hooks/setup.sh into your project's .claude/hooks/ and configure settings.json to invoke it. The hook is idempotent โ€” safe to run alongside launchd/systemd.

Manual. ./bin/whatsapp-mcp serve -addr 127.0.0.1:8765 in any terminal. Ctrl-C to stop.

First-time pairing happens in a browser: start the daemon, open http://127.0.0.1:8765/pair, scan the QR with your phone. No terminal required. WhatsApp's multidevice protocol rotates the linked-device session roughly every 20 days; when that happens, the /pair page serves a fresh QR automatically โ€” visit it again and re-pair. The /pair/* endpoints are rate-limited (5 GET/min, 1 POST/min on /pair/reset) and CSRF-protected.

Flags and environment variables for serve:

  • -addr host:port (env WHATSAPP_MCP_ADDR, default 127.0.0.1:8765).
  • -allow-remote (explicit opt-in to bind a non-loopback address; requires WHATSAPP_MCP_TOKEN).
  • WHATSAPP_MCP_TOKEN โ€” bearer token for /mcp and /pair/* when -allow-remote is set. Required; serve exits if missing.
  • WHATSAPP_MCP_MEDIA_ROOT โ€” allowed root for send_file / send_audio_message media_path and download_media output_path.
  • WHATSAPP_MCP_DEBUG=1 โ€” enable verbose logging with partial phone-number redaction (last 5 digits visible).

Tools

42 tools, grouped by purpose.

Read / query

ToolPurpose
search_contactsSubstring search across cached contact names and phone numbers
list_messagesQuery + filter messages; returns formatted text with context windows
list_chatsList chats with last-message preview; sort by activity or name
get_chatChat metadata by JID
get_message_contextBefore/after window around a specific message
download_mediaDownload persisted media to a local path
request_syncAsk WhatsApp to backfill history for a chat

Send

ToolPurpose
send_messageSend a text message to a phone number or JID
send_fileSend image/video/document/raw audio with optional caption; view_once: bool marks image/video/audio submessages as view-once (ignored for documents)
send_audio_messageSend a voice note (auto-converts via ffmpeg if not .ogg Opus); supports view_once: bool
send_pollSend a poll with a question and 2+ options; selectable_count controls how many options a voter may pick. Generates the 32-byte MessageSecret required for votes to decrypt
send_poll_voteCast a vote on a previously-seen poll; options must match option names exactly
get_poll_resultsReturn the tally for a poll we have cached (includes 0-vote options)
send_contact_cardSend a contact card; synthesises a vCard 3.0 from name + phone, or pass a raw vcard to skip synthesis

Message actions

ToolPurpose
mark_readMark specific message IDs as read
mark_chat_readAck the most recent incoming messages in a chat to clear the unread badge
send_reactionReact to a message (empty emoji clears an existing reaction)
send_replyText reply that quotes a prior message
edit_messageEdit a previously-sent message
delete_messageRevoke (delete for everyone) a message
send_typingSet per-chat composing / recording presence

Groups

ToolPurpose
create_groupCreate a group with a name and initial participants
leave_groupLeave a group
list_groupsList all groups the user is a member of
get_group_infoFull group metadata (participants, settings, invite config)
update_group_participantsAdd / remove / promote / demote participants (action: add|remove|promote|demote)
set_group_nameChange the group subject
set_group_topicChange the group description; empty string clears it
set_group_announceToggle announce-only mode (only admins can send)
set_group_lockedToggle locked mode (only admins can edit group metadata)
get_group_invite_linkGet the invite link; reset: true revokes the previous link first
join_group_with_linkJoin a group via a chat.whatsapp.com URL or bare invite code

Blocklist

ToolPurpose
get_blocklistReturn the current blocklist
block_contactBlock a contact by phone number or JID
unblock_contactUnblock a contact

Privacy / presence / status

ToolPurpose
send_presenceSet own availability (available or unavailable) โ€” distinct from per-chat send_typing
get_privacy_settingsCurrent privacy settings as JSON
set_privacy_settingChange one privacy setting by name + value (strict enum validation; invalid combinations are rejected)
set_status_messageUpdate the profile "About" text; empty string clears it

Admin

ToolPurpose
is_on_whatsappBatch-check which phone numbers are registered on WhatsApp
get_statusReport whether the bridge is connected and which account it's paired as
pairing_statusReport the device-pairing state as a structured setup_state envelope (ready / awaiting_qr + qr_payload / error) for programmatic supervisors that surface the linking QR

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.

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.