WhatsApp MCP Server — Build Plan (Meta Cloud API)
Overview
A private MCP server that exposes WhatsApp messaging capabilities to Claude via the official Meta Cloud API. Claude connects to this server as an MCP client and can send/receive messages, list chats, and query contacts through defined tools.
Architecture
Claude (MCP Client)
│ stdio or SSE transport
▼
WhatsApp MCP Server (Node.js)
│ HTTPS requests
▼
Meta Cloud API (graph.facebook.com)
│
▼
WhatsApp NetworkWebhook flow (incoming messages)
WhatsApp Network
│
▼
Meta Cloud API
│ POST webhook events
▼
Your Webhook Endpoint (public HTTPS URL)
│ writes to queue
▼
In-memory / SQLite queue
│ drained by tool call
▼
get_messages MCP toolPrerequisites
- Meta Developer account at developers.facebook.com
- A WhatsApp Business Account (WABA)
- A verified phone number registered in the WABA
- A public HTTPS URL for the webhook (use ngrok locally, or deploy to a VPS/server)
- Node.js 20+
Required Meta credentials (store in .env)
WA_PHONE_NUMBER_ID= # From Meta App Dashboard
WA_BUSINESS_ACCOUNT_ID= # WABA ID
WA_ACCESS_TOKEN= # Permanent system user token (preferred over temp tokens)
WA_WEBHOOK_VERIFY_TOKEN= # A secret string you define for webhook verification
WEBHOOK_PORT=3000
MCP_TRANSPORT=stdio # or sseProject Structure
whatsapp-mcp/
├── src/
│ ├── index.ts # MCP server entry point — registers tools and starts transport
│ ├── tools/
│ │ ├── send-message.ts # Send a text message to a phone number
│ │ ├── send-template.ts # Send an approved template message
│ │ ├── get-messages.ts # Drain incoming message queue
│ │ ├── list-chats.ts # List recent conversations (from local store)
│ │ ├── get-contact.ts # Look up contact info by phone number
│ │ └── mark-read.ts # Mark a message as read
│ ├── api/
│ │ ├── client.ts # Axios/fetch wrapper for graph.facebook.com/v19.0
│ │ ├── send.ts # POST /messages
│ │ └── types.ts # Meta API request/response types
│ ├── webhook/
│ │ ├── server.ts # Express server handling GET (verify) and POST (events)
│ │ ├── handler.ts # Parse webhook payloads, write to queue
│ │ └── verify.ts # Webhook verification handshake
│ ├── store/
│ │ ├── queue.ts # In-memory or SQLite queue for incoming messages
│ │ └── contacts.ts # Optional local contact name cache
│ └── utils/
│ ├── phone.ts # Normalise phone numbers to E.164 format
│ └── logger.ts
├── .env
├── .env.example
├── package.json
└── tsconfig.jsonMCP Tools to Expose
| Tool | Input | Description |
|---|---|---|
send_message | { to: string, body: string } | Send a plain text message |
send_template | { to: string, template: string, lang: string, components?: [] } | Send a pre-approved template |
get_messages | { limit?: number, markRead?: boolean } | Fetch queued incoming messages |
list_chats | { limit?: number } | List recent contacts/chats from local store |
get_contact | { phone: string } | Return cached contact name and metadata |
mark_read | { messageId: string } | Send a read receipt |
Note: Meta Cloud API does not expose a “list conversations” endpoint.
list_chatsmust be built from your local store of received/sent messages.
Key Implementation Notes
1. Phone number format
Meta requires numbers in E.164 format without the + prefix (e.g. 447700900123). Build a normaliser that strips spaces, dashes, and the leading +:
// utils/phone.ts
export function toMetaFormat(raw: string): string {
return raw.replace(/[^\d]/g, "").replace(/^0+/, "");
}2. Message sending — API call shape
// POST https://graph.facebook.com/v19.0/{PHONE_NUMBER_ID}/messages
{
messaging_product: "whatsapp",
recipient_type: "individual",
to: "447700900123",
type: "text",
text: { body: "Hello from Claude" }
}3. Webhook verification (one-time)
Meta will send a GET request to your webhook URL on setup. Respond with the hub.challenge value only if hub.verify_token matches your env variable.
4. Webhook payload — incoming message shape
{
"object": "whatsapp_business_account",
"entry": [{
"changes": [{
"value": {
"messages": [{
"id": "wamid.xxx",
"from": "447700900123",
"timestamp": "1715000000",
"type": "text",
"text": { "body": "Hello back" }
}],
"contacts": [{ "profile": { "name": "John" }, "wa_id": "447700900123" }]
}
}]
}]
}5. Message queue
Use a simple in-memory array for a start. Migrate to SQLite (better-sqlite3) for persistence across restarts:
// store/queue.ts
const queue: IncomingMessage[] = [];
export const enqueue = (msg: IncomingMessage) => queue.push(msg);
export const drain = (limit = 20) => queue.splice(0, limit);6. Rate limits
Meta Cloud API rate limits are per phone number. Do not send messages in rapid loops. Add a 300–500 ms delay between bulk sends. Free tier is limited to ~1,000 conversations/month.
7. Template messages vs. free-form messages
- You can only send free-form text to users who have messaged you in the past 24 hours.
- Outside that window, you must use an approved template message.
- Always handle this in
send_messageby catching a131047error code (re-engagement required) and suggesting a template instead.
8. MCP transport choice
| Transport | When to use |
|---|---|
StdioServerTransport | Local use with Claude Desktop or Claude Code |
SSEServerTransport | Remote deployment; Claude.ai connects over HTTPS |
9. Startup sequence
1. Load and validate .env
2. Start webhook Express server (for receiving messages)
3. Expose webhook URL to Meta (ngrok tunnel or static domain)
4. Start MCP server with chosen transport
5. Register all tools
6. ReadyGetting Started — Step by Step
- Create Meta App — Go to developers.facebook.com → Create App → Business type → Add WhatsApp product.
- Get test credentials — Use the built-in test number and send a test message from the dashboard to confirm the API token works.
- Scaffold MCP server — Install
@modelcontextprotocol/sdk, implement apingtool, connect via Claude Desktop to verify the transport. - Implement
send_message— Wire the tool to the Meta APIPOST /messagesendpoint. Test end-to-end. - Set up webhook — Run ngrok, paste the public URL into Meta dashboard, implement GET verify + POST handler, confirm delivery receipts appear.
- Implement
get_messages— Drain the queue through an MCP tool. Confirm Claude can read incoming messages. - Add remaining tools — Templates, read receipts, contact cache.
- Harden for production — Replace ngrok with a static HTTPS endpoint, add SQLite queue persistence, add error handling for Meta API error codes.
Dependencies
{
"dependencies": {
"@modelcontextprotocol/sdk": "^1.x",
"express": "^4.x",
"axios": "^1.x",
"better-sqlite3": "^9.x",
"dotenv": "^16.x",
"zod": "^3.x"
},
"devDependencies": {
"typescript": "^5.x",
"@types/node": "^20.x",
"@types/express": "^4.x",
"@types/better-sqlite3": "^7.x"
}
}Meta API Reference Links
- Sending messages:
https://developers.facebook.com/docs/whatsapp/cloud-api/messages - Webhooks setup:
https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks - Error codes:
https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes - Template messages:
https://developers.facebook.com/docs/whatsapp/cloud-api/messages/template-messages - Rate limits:
https://developers.facebook.com/docs/whatsapp/cloud-api/overview/rate-limits