whats-mcp
README.md
# whats-mcp
**MCP server that turns your WhatsApp into agent-callable tools.**
Link your WhatsApp account **once** by scanning a QR code (the official
"Linked Devices" flow — your phone keeps working normally), and the server
keeps the session alive across restarts. Then any MCP-capable agent (Claude
Desktop, Hermes, Cursor, …) can search your contacts, send messages, read
chats, read unread messages, mark things read, send media, and monitor
incoming calls.
Built on **Baileys 6.7.24** (the WhatsApp Web *multi-device* protocol) — a
pure Node.js implementation. **No browser, no Puppeteer, no Chromium.**
> ⚠️ Unofficial client disclaimer: this uses WhatsApp's Web protocol via an
> unofficial library. WhatsApp/Meta can ban accounts that abuse unofficial
> clients. Use it for your own account, at normal human volumes, and avoid
> bulk/spammy automation. No warranty — use at your own risk.
---
## Features
| Area | Tools |
|---|---|
| Linking | `whatsapp_get_qr` (scan to link), `whatsapp_get_pairing_code` (link without QR), `whatsapp_get_status`, `whatsapp_logout` |
| Contacts | `whatsapp_search_contacts` (by name or number) |
| Sending | `whatsapp_send_message` (text / reply), `whatsapp_send_media` (image, audio/voice note, document) |
| Reading | `whatsapp_read_chat` (recent messages), `whatsapp_read_unread` (unread chats + messages), `whatsapp_mark_read` (one chat), `whatsapp_list_chats`, `whatsapp_mark_all_read` (batch read receipts, clears phone badges) |
| Calls | `whatsapp_recent_calls`, `whatsapp_reject_call`, `whatsapp_call` (honest explainer — see Limitations) |
Key properties:
- **Persistent session** — auth lives in `auth_info/` (git-ignored). Restart
the server → reconnects with **no re-scan**.
- **Persistent indexes** — contacts (`contacts.json`) and unread messages
(`unread.json`) are saved to the auth dir, so a daemon restart loses nothing.
- **Auto-reconnect** — exponential backoff on drops; only a *logout* stops it.
- **No browser** — pure WebSocket protocol implementation.
- **Stdio-safe** — QR codes are returned as tool results (raw + ASCII), never
printed to stdout; all logs go to stderr so the JSON-RPC stream stays clean.
## Quick start
> Full step-by-step install guide for **Windows, macOS and Linux** (Node
> setup, QR linking, 24/7 service for each OS): **[docs/INSTALL.md](docs/INSTALL.md)**.
```bash
# 1. install
npm install
# 2. run the server (or: npm start)
node src/index.js
# 3. ask your MCP client to link the device:
# call whatsapp_get_qr → scan the QR in WhatsApp → Settings →
# Linked devices → Link a device
# (or call whatsapp_get_pairing_code with your number and type the code)
```
The server connects to WhatsApp immediately on boot. While unlinked it keeps
a fresh QR ready. Once linked, `creds.json` is persisted — every later start
reconnects automatically.
### Registering in an MCP client
Standard `mcpServers` config (Claude Desktop / Cursor / etc.):
```json
{
"mcpServers": {
"whatsapp": {
"command": "node",
"args": ["/absolute/path/to/whats-mcp/src/index.js"],
"env": {
"WHATSAPP_AUTH_DIR": "/absolute/path/to/whats-mcp/auth_info"
}
}
}
}
```
### Hermes Agent
Connect Hermes to the **daemon** over HTTP (recommended — the systemd service
owns the WhatsApp connection, see below):
```bash
hermes config set mcp_servers.whatsapp.url http://127.0.0.1:8787/mcp
```
or spawn the server per Hermes session (stdio):
```bash
hermes config set mcp_servers.whatsapp.command node
hermes config set mcp_servers.whatsapp.args '["/absolute/path/to/whats-mcp/src/index.js"]'
hermes config set mcp_servers.whatsapp.env.WHATSAPP_AUTH_DIR /absolute/path/to/whats-mcp/auth_info
```
Then restart Hermes — tools appear as `mcp_whatsapp_whatsapp_get_qr`, etc.
(One instance per auth dir: either the daemon **or** a spawned child, never
both — the single-instance lock enforces this.)
### 24/7 daemon (systemd)
Keep the WhatsApp connection alive across reboots and logins:
```bash
# 1. install the unit (see ~/.config/systemd/user/whats-mcp.service in this repo's setup)
systemctl --user daemon-reload
systemctl --user enable --now whats-mcp
loginctl enable-linger "$USER" # start at boot, even before login
```
The daemon serves MCP on `http://127.0.0.1:8787/mcp` (plus `GET /health`),
logs to journald (`journalctl --user -u whats-mcp -f`), and never unlinks the
device on shutdown. Contacts and unread-message state are persisted inside the
auth dir, so restarts lose nothing.
## Environment variables
| Variable | Default | Purpose |
|---|---|---|
| `WHATSAPP_AUTH_DIR` | `./auth_info` | Where the linked-device credentials live (keep private!) |
| `WHATSAPP_LOG_LEVEL` | `warn` | `silent` \| `error` \| `warn` \| `info` \| `debug` — always to **stderr** |
| `WHATSAPP_MARK_ONLINE` | `true` | `false` = don't mark online (phone keeps receiving its own push notifications) |
| `WHATSAPP_MCP_PORT` | *(unset)* | Set to run as a daemon: StreamableHTTP on `127.0.0.1:<port>/mcp` instead of stdio |
| `WHATSAPP_CONNECT_TIMEOUT` | `30` | Seconds send/read tools wait for the connection to be open |
## Tool reference
### `whatsapp_get_qr`
Returns the current linking QR as a raw string **and** an ASCII rendering,
plus seconds until it rotates. Scan with **WhatsApp → Settings → Linked
devices → Link a device**. QR rotates every ~20–60s — scan promptly.
### `whatsapp_get_pairing_code`
`phone` (digits with country code, e.g. `919876543210`) → returns an 8-digit
code. On the phone: **Link a device → Link with phone number instead**, then
enter the code. Fails gracefully if the account doesn't support pairing.
### `whatsapp_get_status`
Connection state, linked flag, QR availability, contact/chat/unread counts,
last disconnect reason. Use this first whenever something seems off.
### `whatsapp_search_contacts`
`query` (name substring or number digits, empty = list all) + `limit` →
matches with `jid`, `name`, `notify`, `number`. The `jid` can be passed to any
tool that takes a `contact`.
### `whatsapp_send_message`
`contact` + `text`, optional `quoted_message_id` (reply). `contact` is
resolved in order: full JID → phone number (verified via `onWhatsApp`) →
saved contact name.
### `whatsapp_send_media`
`contact` + `media_type` (`image` | `audio` | `document`) + `source` (local
path **or** http(s) URL), optional `caption`, `file_name`, `as_voice_note`.
### `whatsapp_read_chat`
`contact` + `limit` → most recent messages seen for that chat (since server
start), plus its unread count.
### `whatsapp_read_unread`
`per_chat` + `max_chats` → chats with `unreadCount > 0`, sorted by unread
count, with the newest incoming messages for each.
### `whatsapp_mark_read`
`contact` → sends read receipts for that chat's tracked messages (respects
the account's read-receipt privacy setting).
### `whatsapp_recent_calls` / `whatsapp_reject_call`
Incoming call events (`offer`/`ringing`/`accept`/`reject`/`timeout`, with
video flag) since server start; reject a live call by id.
### `whatsapp_logout`
Removes the linked device from your account (like tapping "Log out" on the
phone). The auth folder is kept; scan a fresh QR to re-link.
## Limitations (read this — it's honest)
- **Voice/video calls cannot be placed.** This is a hard WhatsApp protocol
limitation: call media flows over a separate signaling path that the
linked-device (Web) protocol does not expose. Verified against the Baileys
6.7.24 API surface — there is no send-side call content; missed-call
"bubbles" are inbound-only server constructs. `whatsapp_call` exists to
explain exactly this. What IS possible: monitor incoming calls
(`whatsapp_recent_calls`) and reject them (`whatsapp_reject_call`).
*(Future: Baileys v7 adds `createCallLink` — sending a tappable call link —
tracked in `docs/ARCHITECTURE.md`.)*
- **History is event-based.** Chats/messages received *after* the server
connects are available immediately; older history arrives via WhatsApp's
`messaging-history.set` sync (contacts + chat metadata, unread counts).
- **Message content**: text, captions and file names are extracted; media
blobs are not downloaded (yet).
- **4 linked devices max** (phone + 3). Scanning a 5th bumps the oldest.
- **The phone must keep WhatsApp installed**; uninstalling or renaming the
number kills the linked session.
## Project layout
```
whats-mcp/
├── src/
│ ├── index.js # entry: boot client, expose over MCP stdio
│ ├── server.js # MCP tool definitions (zod schemas)
│ └── whatsapp.js # Baileys wrapper: QR, auth, reconnect, indexes
├── scripts/
│ └── smoke-test.mjs # stdio end-to-end test (npm run smoke)
├── docs/
│ ├── ARCHITECTURE.md # design decisions + upgrade paths
│ └── AGENTS.md # conventions for agent contributors
├── .env.example
├── package.json
└── LICENSE (MIT)
```
## Testing
```bash
npm run smoke # spawns the real server over stdio, asserts handshake+tools
```
## License
MIT — see [LICENSE](LICENSE). Baileys is MIT; the MCP SDK is MIT.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues