WhatsApp MCP Server
by cascodigital
README.md
# whatsapp-mcp-server
A local WhatsApp message collector plus a Model Context Protocol (MCP) server that lets LLM clients read and send messages on your own WhatsApp account.
  
## Overview
WhatsApp has no official API for reading a personal account's message history, and its built-in
search is unavailable to AI assistants. This project links a companion device with
[Baileys](https://github.com/WhiskeySockets/Baileys), stores every incoming message in a local
SQLite database, and exposes that database to MCP clients (Claude, Codex, and others) over stdio.
Clients can also queue outgoing text or image messages, sent immediately or at a scheduled time.
Messages stay on your machine: there is no cloud component and no HTTP listener.
> This links a real WhatsApp account as a companion device. Use it only with an account you own and
> respect the privacy of everyone in your chats. It is not affiliated with or endorsed by WhatsApp/Meta.
> **Sending safety:** `whatsapp_send_message` sends real messages from **your personal WhatsApp
> account**, to real people, and they cannot be unsent by this server. Configure your MCP client to
> require explicit user confirmation (recipient and content) before every call to this tool, and do
> not auto-approve it.
## Features
- Captures new messages live from the moment the device is paired (full history sync is disabled).
- Keeps the text of messages that are deleted after capture and flags them as revoked.
- Tracks direct and group chats, push names, contacts, group subjects, attachments and links.
- Stores everything in a single SQLite file that is easy to inspect, back up or query directly.
- Exposes 12 MCP tools: 10 read-only tools (status, chat listing and lookup, recent messages, text
search, date ranges, attachments, links, deleted messages, per-chat summaries) plus sending and
outbox status.
- Sends text, or a JPEG/PNG/WebP image (up to 10 MiB) with an optional caption, immediately or
scheduled for a later time, through an outbox queue dispatched by the collector.
- Self-healing collector: exits on disconnect (except logout) or when no connection is established within 120 s,
so the supervisor (Docker `restart: unless-stopped`, systemd) restarts it cleanly; a heartbeat
file drives the Docker healthcheck.
- Imports WhatsApp `.txt` chat exports into the same database.
- Runs natively (Node.js, optionally under tmux or systemd) or in Docker.
## How it works
Two processes share one SQLite database:
```
WhatsApp servers
^ |
sends | | receives
| v
src/collector.js (Baileys linked device, long-running)
| writes messages, reads/updates outbox
v
messages.sqlite <-- src/import_whatsapp_export.js (one-shot .txt import)
^ reads messages, inserts into outbox
|
src/index.js (MCP server over stdio, started on demand by the MCP client)
```
| Process | File | Role |
|-------------|----------------------------------|----------------------------------------------------------|
| collector | `src/collector.js` | Stays connected to WhatsApp, writes messages to the DB and dispatches the outbox |
| mcp | `src/index.js` | Stdio MCP server; queries the DB and enqueues outgoing messages |
| importer | `src/import_whatsapp_export.js` | Parses exported `.txt` chats and inserts them |
Design notes:
- The database uses Node's built-in `node:sqlite` module, so there is no native addon to compile.
- Deleted messages are detected both from `protocolMessage` REVOKE events and from revoke stub
updates. The stored row is marked `isRevoked=true` with `revokedAt`, `revokedBy`, `revokedByMe`
and `revokeSourceMessageId`; the original text is kept.
- Imported messages get synthetic chat IDs (`import:<hash>`) and `source=import`, so they never
mix with live-captured chats.
- The MCP server never talks to WhatsApp directly. `whatsapp_send_message` inserts a row into the
`outbox` table; the collector polls it every second and sends due messages (see
[Sending messages](#sending-messages)).
- The collector identifies itself as `Browsers.macOS('Desktop')`, so the linked device appears
on the phone as macOS.
By default all runtime data lives under `~/.local/share/whatsapp-mcp/`:
```
~/.local/share/whatsapp-mcp/
├── auth/ # Baileys pairing credentials
├── messages.sqlite # message database
├── collector.log
├── collector.pid
└── collector.connected # heartbeat, touched every 30 s while connected
```
## Requirements
- Node.js 22.5 or later (for `node:sqlite`), or Docker with Compose
- A WhatsApp account on a phone, for pairing
- `tmux` (optional, for `bin/start-collector.sh`)
- `python3` (only for the `bin/mcp-*.sh` test helpers)
## Quick start
Native:
1. Install dependencies:
```bash
npm install
```
2. Start the collector in the foreground to display the pairing QR code:
```bash
npm run collector
```
3. On the phone, open **Settings > Linked devices > Link a device** and scan the QR code.
4. Check the result:
```bash
npm run status
```
Output includes `auth=ready` and the database path once pairing succeeded.
Docker:
1. Build and start the collector:
```bash
docker compose up -d --build collector
docker compose logs -f collector
```
2. When the log shows `Scan the QR code in WhatsApp > Linked devices.`, scan it on the phone.
3. List the MCP tools through the container:
```bash
bash bin/mcp-run-list-tools.sh
```
With Docker, data is persisted in `./data` next to `compose.yml` (`./data/auth/`,
`./data/messages.sqlite`, `./data/collector.pid`).
## Configuration
All configuration is through environment variables.
| Variable / key | Required | Default | Description |
|---------------------------------|----------|------------------------------------|---------------------------------------------------------------|
| `WHATSAPP_MCP_HOME` | No | `~/.local/share/whatsapp-mcp` | Base directory for auth, database, log and pid file (`/data` in Docker) |
| `WHATSAPP_BAILEYS_LOG_LEVEL` | No | `warn` | Baileys (pino) log level |
| `WHATSAPP_MCP_TMUX_SESSION` | No | `whatsapp-mcp-collector` | tmux session used by the start/stop/status scripts |
| `WHATSAPP_MCP_SYSTEMD_SERVICE` | No | `whatsapp-mcp-collector.service` | systemd user unit inspected by `bin/status.sh` and the status tool |
| `WHATSAPP_MCP_APP_DIR` | No | repository root | Directory `bin/start-collector.sh` runs the collector from |
| `WHATSAPP_EXPORT_OWNER_NAME` | No | empty | Sender name in `.txt` exports to mark as `fromMe` on import |
| `WHATSAPP_DEFAULT_COUNTRY_CODE` | No | `55` | Prefix added to bare 10/11-digit local numbers when sending; empty disables it |
| `WHATSAPP_MCP_REMOTE_HOST` | For remote | `user@your-docker-host` | SSH target used by `bin/mcp-remote-stdio.sh` |
| `WHATSAPP_MCP_REMOTE_STACK_DIR` | For remote | `/opt/whatsapp-mcp` | Compose stack directory on the remote host |
## Usage
### MCP client configuration
The server speaks stdio, not HTTP.
Native:
```json
{
"command": "node",
"args": ["src/index.js"],
"cwd": "/path/to/whatsapp-mcp-server"
}
```
Docker (uses the `mcp` service in the `manual` profile):
```bash
docker compose run --rm -T mcp
```
Remote Docker host over SSH:
```bash
WHATSAPP_MCP_REMOTE_HOST=user@your-docker-host \
WHATSAPP_MCP_REMOTE_STACK_DIR=/opt/whatsapp-mcp \
bash bin/mcp-remote-stdio.sh
```
### MCP tools
| Tool | Description |
|------------------------------|----------------------------------------------------------------|
| `whatsapp_status` | Collector status, auth state, database stats and local paths |
| `whatsapp_list_chats` | Known chats, newest first |
| `whatsapp_find_chat` | Find chat IDs by contact name, group name or partial JID |
| `whatsapp_recent_messages` | Recent stored messages, optionally for one chat |
| `whatsapp_search_messages` | Case-insensitive substring search over message text |
| `whatsapp_messages_by_date` | Messages in a date/time range, globally or for one chat |
| `whatsapp_list_attachments` | Media/document messages, filterable by chat or type |
| `whatsapp_list_links` | Messages that contain URLs |
| `whatsapp_deleted_messages` | Messages captured and later revoked |
| `whatsapp_summarize_chat` | Statistical summary of a chat (participants, counts, links, media) |
| `whatsapp_send_message` | **Sends** (or schedules) text or an image with caption from your account; require confirmation |
| `whatsapp_outbox_status` | Status of one outbox entry, or the most recent entries |
### Sending messages
`whatsapp_send_message` arguments:
| Argument | Description |
|---------------------|----------------------------------------------------------------------------------|
| `recipient` | Full JID (`<number>@s.whatsapp.net`, `<id>@g.us`), number with country code, or a 10/11-digit local number (prefixed with `WHATSAPP_DEFAULT_COUNTRY_CODE`) |
| `message` | Text, or the caption when an image is sent |
| `image_base64` | Optional JPEG, PNG or WebP as base64 or a `data:image/...;base64,` URL, up to 10 MiB decoded; the format is detected from the file bytes |
| `scheduled_at` | Optional ISO 8601 datetime (e.g. `2026-12-01T09:00:00-03:00`); stored in UTC |
| `wait_for_delivery` | Default `true`: poll up to `timeout_seconds` for the collector to confirm |
| `timeout_seconds` | 1 to 30, default 10 |
Flow:
1. The MCP server validates the input (non-empty text or a valid image), normalizes the recipient
and inserts a `pending` row into the `outbox` table.
2. The collector, while connected, polls the outbox every second and takes up to 5 due messages
(`scheduled_at` empty or in the past), marks each `sending`, and sends it.
3. On success the row becomes `sent` with `sent_at` and the WhatsApp `message_id`, and the stored
image bytes are discarded. On error it becomes `failed` with the error text. Failed messages
are not retried automatically.
Statuses: `pending` (queued, or waiting for its schedule), `sending`, `sent`, `failed`. The tool
returns `scheduled` for future messages, `pending` when not waiting or when the timeout elapsed
before confirmation (the message is still queued and will go out once the collector is connected).
Use `whatsapp_outbox_status` with `outbox_id` to follow up.
### Helper scripts
| Script | What it does | Changes state? |
|------------------------------|-----------------------------------------------------------------|----------------|
| `bin/start-collector.sh` | Starts the collector in a detached tmux session | Yes |
| `bin/stop-collector.sh` | Kills the tmux session and the pid in `collector.pid` | Yes |
| `bin/status.sh` | Prints systemd/tmux/pid state and DB/auth presence | No |
| `bin/mcp-list-tools.sh` | Emits the JSON-RPC lines for `tools/list` | No |
| `bin/mcp-call.sh` | Emits the JSON-RPC lines for one `tools/call` | No |
| `bin/mcp-run-list-tools.sh` | Pipes `mcp-list-tools.sh` into `docker compose run mcp` | No |
| `bin/mcp-run-tool.sh` | Pipes `mcp-call.sh` into `docker compose run mcp` | No |
| `bin/mcp-remote-stdio.sh` | Runs the MCP container on a remote host over SSH | No |
Examples:
```bash
bash bin/mcp-run-tool.sh whatsapp_recent_messages '{"limit":5,"include_from_me":true}'
bash bin/mcp-run-tool.sh whatsapp_search_messages '{"query":"invoice","limit":10}'
bash bin/mcp-run-tool.sh whatsapp_outbox_status '{"limit":5}'
# Native piping without Docker
bash bin/mcp-list-tools.sh | node src/index.js
```
### Importing WhatsApp exports
```bash
WHATSAPP_EXPORT_OWNER_NAME="Your Name" npm run import-export -- "/path/to/exported/chats"
```
The importer walks the folder recursively, parses every `.txt` file (US date format
`M/D/YY, H:MM - Sender: text`), handles multi-line messages, ignores other files such as `.vcf`,
and prints a JSON summary.
### Resetting the pairing (Docker)
```bash
docker compose stop collector && rm -rf data/auth && docker compose up -d collector
```
## Limitations
- No history backfill: only messages received after pairing are captured (plus `.txt` imports).
- If a delete event arrives before the original message is stored, the text cannot be recovered;
the collector logs the revoke for an unknown message.
- Search is a SQL `LIKE` substring match, not ranked full-text search.
- The `.txt` importer only understands the US-style export format (`M/D/YY, H:MM - `). Exports from
other locales are skipped or have day and month swapped.
- Baileys is an unofficial client; WhatsApp protocol changes can break the collector until the
dependency is updated.
- Sending is fire-and-forget: there is no read receipt tracking, no automatic retry of `failed`
messages and no way to cancel a scheduled message through MCP (delete or update the `outbox`
row in SQLite instead). Scheduled messages only go out while the collector is connected.
- The collector exits on disconnect instead of reconnecting in-process, so it needs a supervisor
(Docker restart policy or systemd `Restart=always`). `bin/start-collector.sh` (tmux) does not
restart it.
- `bin/mcp-remote-stdio.sh` disables SSH host key checking (`StrictHostKeyChecking=no`).
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues