Skip to main content
Glama
WhatsApp MCP Server

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.

![License](https://img.shields.io/badge/license-MIT-blue) ![Node.js](https://img.shields.io/badge/node.js-22.5%2B-339933) ![Status](https://img.shields.io/badge/status-maintained-blue)

## 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).