Content
# whatsapp-mcp-server
A local Model Context Protocol (MCP) server that exposes your personal
WhatsApp account as four tools for Claude Desktop or Claude Code.
Backed by [Baileys](https://github.com/WhiskeySockets/Baileys) for the
WhatsApp Web protocol and [better-sqlite3](https://github.com/WiseLibs/better-sqlite3)
for the local message cache.
Single-user, local, no cloud dependencies. Once you scan the QR code
once, your session is saved and you don't have to scan again.
## Features
Four MCP tools:
| Tool | What it does |
|---|---|
| `list_chats` | Recent WhatsApp conversations, newest first. Optional JID-prefix filter (e.g. `120363` for groups). |
| `read_messages` | Last N messages from a chat. Pagination via `beforeTimestamp`. Optional sender filter. |
| `search_messages` | FTS5 full-text search over message history with relevance ranking. Supports `pizza`, `pizza OR burger`, `piz*`, `"exact phrase"`. |
| `send_message` | Send a text message via WhatsApp. The sent message is also persisted locally so subsequent reads/searches return it. |
## Architecture
```
┌─────────────┐
│ Claude Code │ JSON-RPC over stdio
└──────┬──────┘
│
┌──────▼──────────────┐
│ src/index.ts │ MCP server (4 tools registered)
│ src/db/queries.ts │ Read side: listChats, readMessages, searchMessages
│ src/wa/send.ts │ Write side: sendMessage
└──────┬──────────────┘
│
┌──────▼─────────────────┐
│ src/wa/persist.ts │ persistMessage, persistChat, attachPersistence
│ src/wa/socket.ts │ connectToWhatsApp (Baileys socket + QR)
│ src/wa/auth.ts │ loadAuthState
└──────┬─────────────────┘
│
┌──────▼──────────┐
│ data/history.db│ SQLite (chats + messages + FTS5)
│ auth/ │ Baileys session files (gitignored)
└─────────────────┘
```
## Prerequisites
- **Node.js 18+** (this project uses ESM and modern Node APIs)
- **A WhatsApp account on your phone** (you'll scan a QR code once)
- **A C/C++ toolchain** for `better-sqlite3` native build:
- macOS: Xcode Command Line Tools (`xcode-select --install`)
- Linux: `build-essential` + `python3`
- Windows: Visual Studio Build Tools with the "Desktop development with C++" workload
## Install
The package lives on NPM at
[`@muhammadmustafa18/whatsapp-mcp-server`](https://www.npmjs.com/package/@muhammadmustafa18/whatsapp-mcp-server).

> ⚠️ **Don't confuse with `whatsapp-mcp-server` (no scope)** — that's a
> different package by BitNex Technologies that talks to the WhatsApp
> Business Cloud API instead of WhatsApp Web. Always use the scoped
> name with `@muhammadmustafa18/`.
### Option A: Global npm install (recommended)
```bash
npm install -g @muhammadmustafa18/whatsapp-mcp-server
whatsapp-mcp
```


### Option B: From source (development)
```bash
git clone https://github.com/MuhammadMustafa18/whatsapp-mcp-server.git
cd whatsapp-mcp-server
npm install
npm run build
npm start
```
The `whatsapp-mcp` wrapper lives in `bin/cli.mjs`. It resolves a
per-user storage location (`~/.whatsapp-mcp/` by default, overridable
via `WHATSAPP_MCP_HOME`), then spawns the bundled server. This way
your auth + data follow you, not the install location.
## First run
The first time you start the server, WhatsApp will need to authenticate
you. The server prints a QR code to its **stderr** (not stdout — stdout
is reserved for JSON-RPC).
```bash
npm start # Option A
# or
whatsapp-mcp # Option B (if globally installed)
```
You'll see something like:
```
[wa-mcp] starting...
[wa-mcp] db ready at .../data/history.db
[wa-mcp] QR code (scan with WhatsApp):
[QR code here]
```

Open WhatsApp on your phone → **Settings → Linked Devices → Link a Device**,
then scan the QR code. The session is saved and reused on subsequent
runs — no need to scan again unless you log out.
When installed globally, the session is stored at `~/.whatsapp-mcp/auth/`.
When running from a source clone, it's stored at `<project>/auth/`.
Override either with the `WHATSAPP_MCP_HOME` env var.
## Wire it up to Claude
### Claude Code
```bash
# Source clone
claude mcp add whatsapp-mcp -- node /path/to/whatsapp-mcp-server/bin/cli.mjs
# Global install
claude mcp add whatsapp-mcp -- whatsapp-mcp
```


### Claude Desktop
Add this to your Claude Desktop config file:
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"whatsapp": {
"command": "node",
"args": ["/path/to/whatsapp-mcp-server/bin/cli.mjs"]
}
}
}
```
Restart Claude Desktop. You should see "whatsapp" listed in the tools
panel (bottom-left hammer icon).
## Usage examples
Once connected, Claude can use the tools naturally. Some examples:
- "List my recent WhatsApp chats"
- "Show me the last 20 messages with Ali"
- "Search my messages for 'pizza'"
- "Send Ali a message: lunch at 8?"

The tools' full JSON-Schema input definitions are visible to Claude
automatically. See `src/index.ts` for the schema.
## How messages are stored
- Incoming messages (`messages.upsert` event) are persisted into
`messages` and the parent `chats` row is updated with `last_message_at`
and a short `last_message_preview`.
- Chat metadata changes (`chats.update` — name, mute, archive) update
the `chats` row.
- Initial sync (`chats.upsert`) seeds the `chats` table.
- Outgoing messages are also persisted, so `read_messages` returns
both sent and received.
- FTS5 over `messages.text` is kept in sync via SQLite triggers.
## Troubleshooting
### `Cannot find module '...dist/db/schema.sql'`
You forgot to copy the SQL asset. Run `npm run build` (not just `tsc`).
### QR code doesn't show
Make sure you're looking at the **stderr** output, not stdout. stdout
is the JSON-RPC stream. If you redirected both to the same place, the
QR code text would corrupt the protocol.
### `send_message` returns "Cannot read properties of undefined"
The socket isn't actually connected to WhatsApp. This happens if:
- The saved session in `auth/` expired (log out + log in again)
- The phone lost internet connectivity mid-session
- WhatsApp Web is rate-limiting
Restart the server and rescan the QR if needed.
### First-run errors about "no such file or directory" for `auth/`
The `auth/` directory is created automatically on first run. If you're
running the built version (`dist/index.js`), make sure you're running
it from the project root, not from inside `dist/`.
## Uninstall
The package, your auth session, and your message history are all
separate. Pick what you want to remove:
```bash
# Remove the global `whatsapp-mcp` command
npm uninstall -g @muhammadmustafa18/whatsapp-mcp-server
# Remove the MCP entry from Claude Code
claude mcp remove whatsapp-mcp
# Wipe your saved WhatsApp session (forces QR re-scan on next run)
rm -rf ~/.whatsapp-mcp/auth
# Wipe your local message history
rm -rf ~/.whatsapp-mcp/data
# Full reset (everything above + the home folder itself)
rm -rf ~/.whatsapp-mcp
```
On Windows PowerShell use `Remove-Item -Recurse -Force` instead of `rm -rf`,
and `$env:USERPROFILE\.whatsapp-mcp` instead of `~/.whatsapp-mcp`.
If you installed via source clone (with `npm link`) instead of the
global npm install, also drop the local link:
`npm unlink -g @muhammadmustafa18/whatsapp-mcp-server`.
## Development
The project was built one function at a time, with one commit per
function. The git history reads as a tutorial:
```bash
git log --oneline
```
### Project structure
```
src/
├── index.ts # MCP server entrypoint + 4 tool registrations
├── db/
│ ├── index.ts # initDb
│ ├── queries.ts # listChats, readMessages, searchMessages
│ └── schema.sql # SQLite schema (chats, messages, FTS5 + triggers)
└── wa/
├── auth.ts # loadAuthState
├── socket.ts # connectToWhatsApp
├── persist.ts # persistMessage, persistChat, attachPersistence
└── send.ts # sendMessage
```
### Build artifacts
```
dist/ # Compiled JS + copied schema.sql
data/history.db # SQLite (gitignored)
auth/ # Baileys session (gitignored)
```
### Design notes
- **Statement caching**: every `db.prepare` is cached on the db handle
via a `Symbol.for` key, scoped to the db's lifetime. See
`getOrPrepare` in `persist.ts`.
- **FK race in `attachPersistence`**: Baileys sometimes delivers
`messages.upsert` before `chats.upsert` for a brand-new chat. We
wrap the batch in `db.transaction(...)` and `persistChat` first, so
the FK on `messages.chat_jid` is satisfied by the time commit
happens.
- **Dynamic UPSERT in `persistChat`**: built per-call so the SET
clause only touches the fields the caller actually has data for.
Avoids the NOT-NULL-sentinel trap that bit us on the first attempt.
- **FTS5 contentless + triggers**: the `messages_fts` virtual table
is kept in sync via INSERT/UPDATE/DELETE triggers on `messages`,
so we never have to remember to update the search index manually.
## License
ISC