Skip to main content
Glama
excellario

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)