Skip to main content
Glama
Sealjay

mcp-whatsapp

WhatsApp MCP Server

License: MIT GitHub issues

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 @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 — 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) on store/.lock prevents two serve processes 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_message when the input is not already .ogg Opus. Without it, use send_file to 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)

./bin/whatsapp-mcp login

Scan 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.json

  • Cursor: ~/.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 registrations

Process 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-instance serve.

Data flow

  1. The client sends a JSON-RPC tools/call to serve over stdio.

  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 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 0

cleanup.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 0

Why 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

search_contacts

Substring search across cached contact names and phone numbers

list_messages

Query + filter messages; returns formatted text with context windows

list_chats

List chats with last-message preview; sort by activity or name

get_chat

Chat metadata by JID

get_message_context

Before/after window around a specific message

download_media

Download persisted media to a local path

request_sync

Ask WhatsApp to backfill history for a chat

Send

Tool

Purpose

send_message

Send a text message to a phone number or JID

send_file

Send image/video/document/raw audio with optional caption; view_once: bool marks image/video/audio submessages as view-once (ignored for documents)

send_audio_message

Send a voice note (auto-converts via ffmpeg if not .ogg Opus); supports view_once: bool

send_poll

Send 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_vote

Cast a vote on a previously-seen poll; options must match option names exactly

get_poll_results

Return the tally for a poll we have cached (includes 0-vote options)

send_contact_card

Send a contact card; synthesises a vCard 3.0 from name + phone, or pass a raw vcard to skip synthesis

Message actions

Tool

Purpose

mark_read

Mark specific message IDs as read

mark_chat_read

Ack the most recent incoming messages in a chat to clear the unread badge

send_reaction

React to a message (empty emoji clears an existing reaction)

send_reply

Text reply that quotes a prior message

edit_message

Edit a previously-sent message

delete_message

Revoke (delete for everyone) a message

send_typing

Set per-chat composing / recording presence

Groups

Tool

Purpose

create_group

Create a group with a name and initial participants

leave_group

Leave a group

list_groups

List all groups the user is a member of

get_group_info

Full group metadata (participants, settings, invite config)

update_group_participants

Add / remove / promote / demote participants (action: add|remove|promote|demote)

set_group_name

Change the group subject

set_group_topic

Change the group description; empty string clears it

set_group_announce

Toggle announce-only mode (only admins can send)

set_group_locked

Toggle locked mode (only admins can edit group metadata)

get_group_invite_link

Get the invite link; reset: true revokes the previous link first

join_group_with_link

Join a group via a chat.whatsapp.com URL or bare invite code

Blocklist

Tool

Purpose

get_blocklist

Return the current blocklist

block_contact

Block a contact by phone number or JID

unblock_contact

Unblock a contact

Privacy / presence / status

Tool

Purpose

send_presence

Set own availability (available or unavailable) — distinct from per-chat send_typing

get_privacy_settings

Current privacy settings as JSON

set_privacy_setting

Change one privacy setting by name + value (strict enum validation; invalid combinations are rejected)

set_status_message

Update the profile "About" text; empty string clears it

Admin

Tool

Purpose

is_on_whatsapp

Batch-check which phone numbers are registered on WhatsApp

get_status

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 login when that happens.

  • Message gaps when serve isn'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 use request_sync per chat, or accept the loss.

  • Single instance per store: only one whatsapp-mcp serve can hold the store lock. Parallel MCP clients must point at different -store directories (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 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 … on serve — run ./bin/whatsapp-mcp login first. serve cannot display a QR because its stdout is reserved for MCP JSON-RPC.

  • another whatsapp-mcp instance is already running — only one serve can hold the store lock. Check for a stray process (ps aux | grep whatsapp-mcp) or another MCP client pointed at the same -store directory.

  • 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_sync to target a specific chat.

  • WhatsApp out of sync — delete both database files (store/messages.db and store/whatsapp.db) and re-run login.

  • ffmpeg not foundsend_audio_message needs ffmpeg on PATH to convert non-Opus audio. Use send_file for 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.

Install Server
A
security – no known vulnerabilities
A
license - permissive license
A
quality - A tier

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