Skip to main content
Glama
eusoubrasileiro

whatsapp-mcp

README.md
# WhatsApp MCP Server

WhatsApp as an MCP server. Runs as a long-lived Docker daemon, accessed over HTTPS with Bearer auth, and pushes [ntfy](https://ntfy.sh) notifications when your session needs attention (QR expired, connection dropped). Built on [Baileys](https://github.com/WhiskeySockets/Baileys) via the `@amiticia/baileys-client` wrapper, which is vendored in this repository as a workspace package under [`packages/baileys-client/`](./packages/baileys-client/) — there is no private dependency to resolve.

> **▶ Start here:** [`src/stream/follow.ts`](./src/stream/follow.ts) — the smallest
> file that shows what this server is actually for: turning an inbound WhatsApp
> chat into a stream an agent can be woken by, instead of a mailbox it has to poll.

## Look at first

1. **[`src/stream/follow.ts`](./src/stream/follow.ts)** — the `follow_chat` tool.
   An agent that must "be present" in a conversation for hours cannot hold a turn
   open and cannot afford to poll; this hands it a stream URL its harness
   subscribes to, and the agent is woken per message. The three-way choice
   between a bounded block, a session-length stream and a deployment-length
   webhook is the whole design question for a reactive messaging tool, and this
   file is where it is answered.
2. **[`docs/spec-agent-presence-stream.md`](./docs/spec-agent-presence-stream.md)** —
   the spec behind that file: why presence is a distinct lifetime from
   request/response, and what the stream must guarantee so a woken agent never
   silently drops a message.
3. **[`docs/account-restrictions.md`](./docs/account-restrictions.md)** — the
   unglamorous one, and the one worth reading. An unofficial WhatsApp client can
   get a real account banned. This documents the constraints that shaped the send
   path — pacing, typing simulation, a send blocklist, a cold-contact guard —
   which is why `src/send-pacer.ts`, `src/send-guard.ts`, `src/send-blocklist.ts`
   and `src/cold-contact.ts` exist at all.

## Who this is for

You want your personal WhatsApp account reachable as a set of tools from **Claude Code, Claude Desktop, Cursor, or any custom MCP/HTTP client** — from any machine — with the WhatsApp connection surviving client restarts, multiple clients sharing one socket, and a push telling you on your phone when you need to re-scan a QR.

## Features

- **HTTP MCP endpoint** (`httpStream` transport) — connect from anywhere, share the session across multiple clients without Baileys fighting for the socket
- **Bearer-token auth** on the MCP endpoint
- **Public QR web page** (protected by paired-number check) — tap the ntfy push and scan directly from your phone browser
- **ntfy push** on: first QR after disconnect, every 2min while still waiting, connection drop, reconnect after drop, and unexpected pairings
- **Bad-pairing protection** — if someone else scans the public QR, the app auto-logs out and purges credentials (`EXPECTED_WA_NUMBER`)
- **23 MCP tools** — search contacts/messages, list chats, send text/media, react, delete, mark read, download media, plus webhook subscriptions for real-time inbound push (see table below)
- **Persistent SQLite** (chats/messages/contacts) and Baileys multi-file auth stored in a Docker volume

## Architecture

```
                    https (Bearer)
  MCP clients  ───────────────────▶  Traefik  ──▶  :39001 (FastMCP httpStream)
  (Claude Code, Desktop,                                │
   Cursor, custom agents)                               │
                                                        ▼
                    https (public)                 whatsapp-mcp
  Phone browser ───────────────▶  Traefik  ──▶  :39002 (QR page)
                                                        │
                                                        ▼
                                                    Baileys
                                                        │ WA Web API
                                                        ▼
                                                  WhatsApp servers

                                                   outbound only ▲
                                                        │
                                                       POST
                                                        │
                                                     ntfy.sh
                                                        │ push
                                                        ▼
                                                     Your phone
```

One container exposes two HTTP servers on different ports. Traefik terminates TLS (Let's Encrypt) and routes by hostname.

## Quick start for MCP clients

The canonical deployment exposes:

- `https://mcp.example.com/mcp` — MCP endpoint, requires `Authorization: Bearer <MCP_AUTH_TOKEN>`
- `https://wa.example.com/` — QR web page

If you run your own instance, replace hostnames accordingly.

### Claude Code

Edit `~/.claude.json`, in the top-level `mcpServers` object:

```json
"whatsapp": {
  "type": "http",
  "url": "https://mcp.example.com/mcp",
  "headers": {
    "Authorization": "Bearer ${MCP_AUTH_TOKEN}"
  }
}
```

Export `MCP_AUTH_TOKEN` in your shell (or put the token literally — `~/.claude.json` is `0600`). Restart Claude Code, run `/mcp` — should show `whatsapp: ✓ Connected`.

### Claude Desktop

`~/.config/Claude/claude_desktop_config.json` (Linux) / `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):

```json
{
  "mcpServers": {
    "whatsapp": {
      "type": "http",
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "Bearer your-token-here" }
    }
  }
}
```

### Cursor

`~/.cursor/mcp.json` — same shape as Claude Desktop above.

### Other clients / custom code

See [`examples/`](./examples/) for a raw HTTPS JSON-RPC transcript (curl), a Python client, and a custom TypeScript agent using `StreamableHTTPClientTransport`.

## MCP tools

The server exposes 23 tools. Full details are in [`CLAUDE.md`](./CLAUDE.md).

| Category | Tools |
|----------|-------|
| Connection / Auth | `get_connection_status`, `logout` |
| Contacts | `search_contacts`, `list_contacts` |
| Messages | `list_messages`, `get_messages_today`, `search_messages`, `get_message_context` |
| Reactive monitoring | `get_new_messages` (cursor delta), `wait_for_messages` (long-poll — block until a reply lands), `follow_chat` (session-length stream: returns a URL your harness subscribes to, so the agent is woken per message instead of occupying a turn) |
| Chats | `list_chats`, `get_chat` |
| Groups | `get_group_info` |
| Sending | `send_message`, `send_file` |
| Actions | `react_to_message`, `delete_message`, `mark_chat_read` |
| Media | `download_media` (audio → transcription by default; image → opt-in description) |
| Webhooks | `register_webhook`, `deregister_webhook`, `list_webhooks` (real-time inbound push to a reactive agent; allow-listed chats; HMAC-signed) |

## Deployment

[`deploy/README.md`](./deploy/README.md) describes the reference topology: the two
HTTP servers, the four Traefik routes, the volumes and the auth surface.

**What is deliberately not here.** The host-specific operations runbook — SSH
targets, DNS records, backup and restore procedures, credential rotation — is not
part of this public repository, and neither is the private container registry the
image is normally published to. `deploy/docker-compose.yaml` is a sanitised
topology reference, not a deployable manifest for anyone else's host.

The image builds from this repository alone; `packages/baileys-client` is a
workspace package, so no sibling checkout and no build context are needed:

```bash
docker build -t whatsapp-mcp:latest .
```

## Local development

For development without Docker, keep the default stdio transport:

```bash
pnpm install
pnpm test       # vitest — 687 pass, 4 skipped; must stay green before commits (extreme TDD)
pnpm typecheck
pnpm start      # node --experimental-strip-types src/main.ts
```

Requires Node.js `>= 23.10.0` for `--experimental-strip-types` and native `better-sqlite3`.

For local HTTP mode (same as production minus Traefik):

```bash
MCP_TRANSPORT=httpstream MCP_AUTH_TOKEN=dev pnpm start
# then: curl -H 'Authorization: Bearer dev' http://127.0.0.1:39001/mcp ...
```

See [`scripts/smoke-test.sh`](./scripts/smoke-test.sh) for the auth matrix.

## Environment variables

See the full table in [`CLAUDE.md#environment-variables`](./CLAUDE.md). Highlights:

| Variable | Purpose |
|----------|---------|
| `MCP_AUTH_TOKEN` | Bearer token required by the HTTP MCP endpoint (mandatory in production) |
| `NTFY_TOPIC_URL` | Unset = no push notifications; set to enable |
| `EXPECTED_WA_NUMBER` | Prefix allowed to pair; wrong scan → auto-logout + purge (strongly recommended when `wa.example.com` is public) |
| `WHATSAPP_MCP_DATA_DIR` | Base dir for `auth_info/`, `data/`, and logs (defaults to `.`, Docker uses `/data`) |
| `OPENROUTER_API_KEY` | Whisper provider for `download_media` audio transcription |
| `AUDIO_PROVIDER` | Transcription route: `openrouter` (default) \| `groq` \| `openai`. Rollback lanes only — the route is chosen by this var, never by which key happens to be set |
| `GEMINI_API_KEY` | Required for `download_media({ describe: true })` image captioning via Gemini 2.5 Flash |

## Data storage & privacy

- **Credentials**: `WHATSAPP_MCP_DATA_DIR/auth_info/` (Baileys multi-file auth state)
- **Messages / chats / contacts**: `WHATSAPP_MCP_DATA_DIR/data/whatsapp.db` (SQLite via Drizzle + `better-sqlite3`)
- **Media**: served from a RustFS sidecar on the same VPS, behind Traefik at `https://mcp.example.com/media/<key>`. The `download_media` tool returns an MCP `resource_link` pointing at that URL (publicly fetchable, no Bearer needed) plus inline `imageContent`/`audioContent` on the first call. Cache hits return the URL only.
- **Audio → text**: by default, `download_media` on an audio/ptt message transcribes via OpenRouter Whisper (`openai/whisper-large-v3`) after preprocessing to 16 kHz mono FLAC. The response is wrapped in an `<transcription>` XML block. Pass `transcribe: false` to get raw audio bytes instead. Requires `OPENROUTER_API_KEY`. Groq and OpenAI remain as rollback routes via `AUDIO_PROVIDER`, but both accounts are scheduled for closure — once they are, those routes stop working and this line should say so.
- **Image → text**: opt-in via `download_media({ ..., describe: true })`. Sends bytes to Gemini 2.5 Flash; response wrapped in an `<image_description>` XML block. Requires `GEMINI_API_KEY`.
- **Logs**: `WHATSAPP_MCP_DATA_DIR/{wa,mcp}-logs.txt` (pino JSON lines)

Everything stays on the VPS (Docker bind mount in production, filesystem in dev). Data leaves the VPS only when an MCP client explicitly invokes a tool.

All data directories are `.gitignore`d. Treat them as sensitive — anyone with `auth_info/` can impersonate your WhatsApp session.

## About this public export

This repository is published as a single squashed commit rather than with its
development history, and some content was removed rather than redacted:

- **No session credentials, no message database, no logs.** `auth_info/`,
  `data/whatsapp.db` and the `*-logs.txt` files were never in the tree or in any
  commit, and remain gitignored. Anyone holding `auth_info/` can impersonate the
  WhatsApp session it belongs to.
- **No host-specific deployment runbook** — see the Deployment section above.
- **Phone numbers, group ids and `@lid` identifiers** in tests, fixtures and
  examples are synthetic placeholders. They keep the real wire format so the
  identifier-canonicalisation tests still parse them, but they identify nobody.
- **`deploy/.env.example`** is a template: every variable is empty, with the
  `openssl rand` command that generates it. No secret material.

## Credits

- Conceptual origin: [lharries/whatsapp-mcp](https://github.com/lharries/whatsapp-mcp) (Go + Python).
- Fork history: started from `jlucaso1/whatsapp-mcp-ts`, then heavily rewritten.

## License

MIT — see [`LICENSE`](LICENSE), matching `package.json`.