Skip to main content
Glama
ABrauser

WhatsApp MCP Server (Docker Edition)

by ABrauser
README.md
# WhatsApp MCP Server (Docker Edition)

A WhatsApp MCP (Model Context Protocol) server that runs as a Docker container and is accessible over HTTP/SSE. Based on [whatsapp-mcp-ts](https://github.com/jlucaso1/whatsapp-mcp-ts) with the transport layer changed from stdio to SSE for remote access.

## Features

- **8 MCP Tools**: search_contacts, list_messages, list_recent_messages, list_chats, get_chat, get_message_context, send_message, search_messages
- **Docker-first**: Runs as a container, accessible via IP
- **Streamable HTTP Transport**: stateless MCP over HTTP POST
- **Bearer-token auth** for `/sse` (set `MCP_AUTH_TOKEN`)
- **Persistent Data**: SQLite DB (WAL + FTS5 search) and WhatsApp auth in Docker volumes
- **Health Check**: `/health` returns 503 when WhatsApp is disconnected
- **Multi-Arch**: Builds for both amd64 and arm64

## Quick Start

### 1. Configure secrets

```bash
cp .env.example .env
# Generate a strong token and write it to .env:
echo "MCP_AUTH_TOKEN=$(openssl rand -hex 32)" >> .env
```

### 2. Pull and Run

```bash
# Pull the image
docker pull ghcr.io/YOUR-USERNAME/whatsapp-mcp-docker:latest

# Run with docker compose
docker compose up -d

# Watch logs for QR code
docker logs -f whatsapp-mcp
```

### 3. Scan QR Code

On first start, a QR code URL will appear in the logs. Open it in your browser and scan with WhatsApp (Settings → Linked Devices → Link a Device).

### 4. Configure MCP Client

This server speaks **Streamable HTTP** MCP (POST `/sse`), not legacy SSE. The exact config key depends on your client:

**Gemini CLI** (`~/.gemini/settings.json` — the key is `httpUrl`, **not** `url`):

```json
{
  "mcpServers": {
    "whatsapp": {
      "httpUrl": "http://192.168.0.101:3010/sse",
      "headers": {
        "Authorization": "Bearer YOUR_TOKEN_FROM_DOTENV"
      }
    }
  }
}
```

> ⚠️ **Common pitfall:** Using `"url"` in Gemini CLI selects the *legacy SSE* transport, which this server does not implement. You will get 401 errors that look like an auth problem but are actually a transport mismatch. Use `"httpUrl"`.

**Claude Desktop / Cline / Continue / Cursor** (most use `url` for Streamable HTTP):

```json
{
  "mcpServers": {
    "whatsapp": {
      "url": "http://192.168.0.101:3010/sse",
      "headers": {
        "Authorization": "Bearer YOUR_TOKEN_FROM_DOTENV"
      }
    }
  }
}
```

## Contact Name Resolution

WhatsApp Web (Baileys) does not give us your phone-side address book directly. To still show *your* saved names — not the contact's self-chosen push names — the server uses three layers, in this order:

1. **Manual override** (`contact_overrides.json`, see below) — wins everything.
2. **Auto-linking** — every incoming message's `pushName` is captured and stored as the `notify` of the corresponding `@s.whatsapp.net` contact. This lets the existing fuzzy-match in queries automatically connect the `@lid` form (used in groups & status updates) to the `@s.whatsapp.net` row that holds your saved address-book name. After enough message activity, most contacts resolve themselves.
3. **Fallback** — `name` → `notify` → phone digits → JID.

### Manual overrides (`contact_overrides.json`)

Use this **only** for contacts that auto-linking can't fix (e.g. a contact who has never written you directly, or whose push name and saved name are completely different and the fuzzy match doesn't catch it).

Path: `<data dir>/contact_overrides.json` (typically `/opt/docker/whatsapp-mcp/data/contact_overrides.json` on the host)

```json
{
  "210444891463794@lid": "Tammy",
  "11390320418995@lid": "Amelie",
  "4917697335710@s.whatsapp.net": "Tammy"
}
```

- The override wins over the DB-resolved name in every MCP response (`chat_name`, `sender_name`, `last_sender_name`).
- File is **hot-reloaded** on save — no container restart required.
- Each entry is a JID → display name mapping. Find a contact's JID via `search_contacts` or `list_chats`.
- See `contact_overrides.example.json` in the repo for the format.

## Security

- **Always set `MCP_AUTH_TOKEN`** unless you bind the container to `127.0.0.1` only. Without a token, anyone on your network can read your chats and send messages from your account.
- Without TLS, the token is sent in plaintext. For LAN-only deployments this is usually acceptable; for anything wider, terminate TLS in a reverse proxy (Caddy, Traefik, nginx).
- Logs in `/app/data/wa-logs.txt` and `/app/data/mcp-logs.txt` may contain message snippets. Treat the data volume as sensitive.
- The QR code from initial pairing is logged at `info` level once — it grants full session access for a few seconds. Don’t share that log.

## Endpoints

| Endpoint | Method | Auth | Description |
|----------|--------|------|-------------|
| `/sse` | POST | Bearer | Streamable HTTP endpoint for MCP (stateless) |
| `/health` | GET | none | 200 when WhatsApp connected, 503 otherwise |

## MCP Tools

| Tool | Description |
|------|-------------|
| `search_contacts` | Search contacts by name or phone number |
| `list_messages` | Get message history for a chat (paginated) |
| `list_chats` | List chats sorted by activity or name |
| `get_chat` | Get details of a specific chat |
| `get_message_context` | Get messages around a specific message |
| `list_recent_messages` | Cross-chat time-window query (e.g. "last 8 hours") with optional ISO `since`/`until` |
| `send_message` | Send a text message to a user or group |
| `search_messages` | Full-text search across messages |

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `MCP_PORT` | `3010` | HTTP server port |
| `MCP_AUTH_TOKEN` | _(unset)_ | Bearer token required on `/sse`. If unset, endpoint is open. |
| `LOG_LEVEL` | `info` | Log level (debug, info, warn, error) |
| `WHATSAPP_MCP_DATA_DIR` | `/app/data` | SQLite database directory |
| `WHATSAPP_AUTH_DIR` | `/app/auth_info` | WhatsApp auth credentials directory |
| `TZ` | `Europe/Berlin` | Timezone used for human-readable timestamps |
| `WHATSAPP_DEBUG` | _(unset)_ | If `true`, prints @lid mapping diagnostics on startup |

## Development

Requires **Node.js >= 24** (stable `node:sqlite` and on-by-default TypeScript stripping). On Node 22/23, run with `--experimental-sqlite --experimental-strip-types`.

```bash
# Install dependencies
npm install

# Run locally (Node 24+)
node src/main.ts

# Run locally (Node 22/23)
node --experimental-sqlite --experimental-strip-types src/main.ts

# Type-check only
npx tsc --noEmit

# Build Docker image locally
docker build -t whatsapp-mcp-docker .
```

## Architecture

```
AI Client (Gemini CLI, Claude, etc.)
    │
    │ HTTP POST (Streamable)
    ▼
┌─────────────────────┐
│  Express Server     │  Port 3010
│  ├── POST /sse      │  Stateless MCP
│  └── GET /health    │  Health check
├─────────────────────┤
│  MCP Server         │  7 Tools
├─────────────────────┤
│  Baileys            │  WhatsApp Web API (Reconnects + Backoff)
├─────────────────────┤
│  SQLite             │  Messages & Contacts (Batch Sync)
└─────────────────────┘
```

## Credits

Based on [whatsapp-mcp-ts](https://github.com/jlucaso1/whatsapp-mcp-ts) by jlucaso1.