whatsapp-mcp-server
by excellario
README.md
# whatsapp-send-mcp
A least-privilege, **send-only** [Model Context Protocol](https://modelcontextprotocol.io) server for WhatsApp.
It exposes **two send tools** and nothing else, backed by Meta's official [WhatsApp Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api):
- `send_whatsapp_message` — send a free-form **text** message (deliverable only inside WhatsApp's 24-hour customer service window).
- `send_whatsapp_template` — send a pre-approved **template** message (deliverable **at any time**, the reliable path for scheduled notifications).
That is the entire surface area:
- It **can** send a text or template message to a specified or default recipient.
- It **cannot** read messages, list chats, access contacts, read statuses, or download/upload media. None of that is implemented, and the Graph API token scope it uses (`whatsapp_business_messaging`) does not grant a "read messages" capability in the first place. Send-only is enforced by the platform, not just by this wrapper.
## Why the official Cloud API (and not Baileys / whatsapp-web.js)
- Official and Terms-of-Service compliant.
- Does not require linking your personal WhatsApp account as a device.
- The API has no endpoint to read a user's message history, so a send-only integration is enforced at the platform level.
## What it does not do (by design)
- No inbound message handling or webhooks.
- No reading of message history, chats, contacts, or statuses.
- No media upload or download.
- No group messaging.
- No template **management** from the server (creating/editing/deleting templates). Templates are created once in WhatsApp Manager, or via the included one-off `create:template` script; the server only *sends* already-approved templates.
These are deliberate non-goals. Adding a read capability later would be a separate, deliberate decision, not something bundled into this package.
## Requirements
- Node.js 20 or newer.
- A WhatsApp Cloud API app on [Meta for Developers](https://developers.facebook.com), with an access token and a phone number ID.
## Install
Run directly with `npx` (no install needed):
```bash
npx @praise25/whatsapp-mcp-server
```
Or install globally:
```bash
npm install -g @praise25/whatsapp-mcp-server
```
## Configuration
All configuration is via environment variables. Nothing is hardcoded, and the access token is never logged.
| Variable | Required | Description |
| --- | --- | --- |
| `WHATSAPP_ACCESS_TOKEN` | Yes | Meta system-user or temporary access token, scoped to `whatsapp_business_messaging`. |
| `WHATSAPP_PHONE_NUMBER_ID` | Yes | The Cloud API **phone number ID** (a numeric ID, not the phone number itself). |
| `WHATSAPP_DEFAULT_RECIPIENT` | No | Default recipient in E.164 format (e.g. `+2348012345678`), used when `to` is omitted. |
| `WHATSAPP_GRAPH_API_VERSION` | No | Graph API version segment. Defaults to `v21.0`. |
The server validates required variables at startup and **fails fast** with a clear error if any are missing. See [`.env.example`](.env.example).
## How to get a WhatsApp Cloud API test number and access token
1. Go to [Meta for Developers](https://developers.facebook.com/) and create (or open) an app of type **Business**.
2. Add the **WhatsApp** product to the app.
3. Open **WhatsApp > API Setup**. Meta provides a free **test phone number** for development.
4. Copy the **Phone number ID** shown there into `WHATSAPP_PHONE_NUMBER_ID`.
5. Copy the **temporary access token** into `WHATSAPP_ACCESS_TOKEN`. (Temporary tokens last ~24 hours. For a longer-lived token, create a **System User** in Meta Business Settings and grant it the `whatsapp_business_messaging` permission only.)
6. Still on the **API Setup** page, add your own phone number to the **allowed recipient list** so the test number is permitted to message you.
7. Messages sent from a test number can only go to numbers on that allowed list until the number is fully verified for production.
## The one tool: `send_whatsapp_message`
**Input:**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `to` | string | Optional if `WHATSAPP_DEFAULT_RECIPIENT` is set | Recipient in E.164 format, e.g. `+2348012345678`. |
| `message` | string | Yes | The text body to send. |
**Output:**
- On success: the WhatsApp message ID.
- On failure: a distinguishable, human-readable error (authentication, invalid recipient, rate limit, server error, network error), not a generic "failed".
Behavior notes:
- Calls `POST https://graph.facebook.com/v21.0/{PHONE_NUMBER_ID}/messages` with a `type: "text"` payload.
- Retries **at most once**, and only on a 5xx or network error. It never retries on a 4xx (those are permanent: bad token, bad number, etc.).
> **The 24-hour window.** A free-form text message is only *delivered* if the recipient has messaged your number within the last 24 hours. Outside that window the API still returns success, but WhatsApp drops delivery. For reliable, unprompted delivery, use a template (below).
## The other tool: `send_whatsapp_template`
Sends a **pre-approved template**, which WhatsApp delivers **at any time**, with no 24-hour-window dependency. This is the reliable choice for scheduled notifications, alerts, and digests.
**Input:**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `to` | string | Optional if `WHATSAPP_DEFAULT_RECIPIENT` is set | Recipient in E.164 format. |
| `templateName` | string | Yes | An **approved** template name (the built-in `hello_world` always exists). |
| `languageCode` | string | No | BCP-47 code the template was approved in. Defaults to `en_US`. |
| `headerParams` | string[] | No | Ordered values for the header's `{{1}}, {{2}}, …` placeholders. |
| `bodyParams` | string[] | No | Ordered values for the body's `{{1}}, {{2}}, …` placeholders. Each value must be a **single line** (no newlines/tabs, WhatsApp rejects them inside a variable). |
The server stores none of the template wording. Meta owns the approved template; the caller supplies only the variable values that fill its blanks.
### Registering a template
Templates are created once in [WhatsApp Manager](https://business.facebook.com/wa/manage/message-templates) (Create template), or reproducibly via the included one-off script:
```bash
npm run create:template
```
This posts a template to your WABA and requires the token to also have the `whatsapp_business_management` permission (sending only needs `messaging`). After submission, wait for the status to become **APPROVED** before sending it. Note WhatsApp's *variable-to-text ratio* rule: a body with too many variables relative to its fixed wording is rejected, add more fixed text or fewer variables.
## Use with an MCP client
### Claude Desktop / Claude Code
Add to your MCP client configuration (e.g. `claude_desktop_config.json`):
```json
{
"mcpServers": {
"whatsapp-send": {
"command": "npx",
"args": ["-y", "@praise25/whatsapp-mcp-server"],
"env": {
"WHATSAPP_ACCESS_TOKEN": "your-access-token",
"WHATSAPP_PHONE_NUMBER_ID": "1234567890",
"WHATSAPP_DEFAULT_RECIPIENT": "+2348012345678"
}
}
}
}
```
The server communicates over stdio.
## Remote deployment (HTTP transport)
For unattended / scheduled use, the server can also run as an always-on HTTPS service and be added to Claude as a **custom connector** (Settings → Connectors → Add custom connector → give it the public URL). It exposes a **Streamable HTTP** endpoint at `/mcp`.
**Security: the HTTP endpoint requires the secret token.** A public send endpoint must be authenticated, otherwise anyone who finds the URL could send WhatsApp messages on your account. The server refuses to start over HTTP without `MCP_AUTH_TOKEN` set. It accepts the token two ways:
- **Header (preferred):** `POST /mcp` with `Authorization: Bearer <MCP_AUTH_TOKEN>`. Use this whenever your client can set headers (e.g. Claude Code's `--header`). The token never appears in the URL.
- **Path (fallback):** `POST /mcp/<MCP_AUTH_TOKEN>`. For connector UIs that only accept a URL and cannot attach a header. The URL then *is* the secret, treat it accordingly (see SECURITY.md): use the long hex token, do not share or screenshot the URL, and be aware the host's access logs may record the path.
Extra env vars for HTTP mode:
| Variable | Required | Description |
| --- | --- | --- |
| `MCP_TRANSPORT` | Yes (`http`) | Set to `http` to run the HTTP server (default `stdio`). |
| `MCP_AUTH_TOKEN` | Yes | Long random secret. Callers must present it as a bearer token. Generate: `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`. |
| `PORT` | No | Port to listen on. Hosts like Render set this automatically. |
Endpoints:
- `POST /mcp` — the MCP endpoint (bearer auth required).
- `GET /health` — unauthenticated health check, also useful for a keepalive ping to stop a free host from sleeping.
**Keepalive (no external pinger needed).** On a free host that sleeps when idle, the server can keep *itself* awake by self-pinging `/health` on a timer. On Render this turns on automatically (it reads `RENDER_EXTERNAL_URL`). Elsewhere, set `KEEPALIVE_URL` to your public base URL. Tune the interval with `KEEPALIVE_MINUTES` (default 10; the host sleeps after ~15 min idle). Note a single always-on free Render service uses roughly 730 of the 750 free monthly hours.
### Deploy to Render
A [`render.yaml`](render.yaml) blueprint is included. Push this repo to GitHub, then in Render: **New → Blueprint** and point it at the repo. Set the secrets (`MCP_AUTH_TOKEN`, `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_PHONE_NUMBER_ID`, `WHATSAPP_DEFAULT_RECIPIENT`) in the Render dashboard, never in the file. Render health-checks `/health`; a separate keepalive ping to `/health` keeps a free instance awake.
## Local development
```bash
npm install
npm run build # compile TypeScript to dist/
npm test # run unit tests (fetch is mocked; no real API calls)
```
### Manual send tests
To verify against the real Graph API before publishing, put your credentials in a local `.env` (copy from `.env.example`) and run any of these. They actually deliver messages, so use a number you control.
```bash
npm run test:send # free-form text to WHATSAPP_DEFAULT_RECIPIENT
npm run test:send -- +2348012345678 # text to an explicit recipient
npm run test:template # built-in hello_world template (works anytime)
npm run create:template # one-off: register the example topic_digest_report template
npm run test:digest # send that template once approved
npm run test:digest -- "AI News" "Morning" "OpenAI shipped X • DeepMind announced Y"
```
`test:template` and `test:digest` demonstrate template delivery, which works **regardless** of the 24-hour window.
## Security
See [SECURITY.md](SECURITY.md). In short: the access token is read only from an environment variable, is never committed and never logged, and **revoking it in the Meta dashboard immediately disables this integration entirely**.
## License
[MIT](LICENSE)
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues