Skip to main content
Glama
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.

Maintenance

ActivitySlowing
ResponsivenessNo issues