WhatsApp MCP Server — Plan
A private, multi-tenant-ready MCP server exposing WhatsApp messaging via the Meta Cloud API. Built for the owner’s own projects first, then for explicitly-approved external clients. Security and internal use are the top priority.
Supersedes the single-tenant SQLite sketch in ../whatsapp-mcp-plan.md with Postgres, multi-tenant API-key auth, remote MCP transport, Inngest Cloud, and Docker Compose deployment — while keeping a clean local-stdio dev mode for the owner.
Project status
Current phase: Phase 1 — Skeleton (stdio). Implementation is in place and CI is green on main. Two DoD items remain open: registering the wamcp_ custom pattern with GitHub Secret Scanning, and the manual end-to-end check of Claude Code calling ping over stdio.
Each phase file has a Definition of Done checklist at the bottom — open the phase file, tick boxes as work lands, the phase is considered done when every box (including the final signoff) is ticked.
| Phase | Status | Done when |
|---|---|---|
| Phase 0 — Meta setup | ⬜ Not started | Meta App + WABA + token + DNS + host baseline + Inngest Cloud all green |
| Phase 1 — Skeleton (stdio) | 🟡 In progress | pnpm dev boots stdio MCP; Claude lists tools + calls ping; CI green |
| Phase 2 — Outbound + webhook | ⬜ Not started | send_message + reply round-trip with rows in messages |
| Phase 3 — Inngest | ⬜ Not started | Webhook returns 200 in < 50ms; sends queued + durable across restarts |
| Phase 4 — Auth + audit | ⬜ Not started | Second client cannot exceed scope/grant/rate; every call audited |
| Phase 5 — Remote MCP | ⬜ Not started | Remote Claude receives notifications/resources/updated within ~1s |
| Phase 6 — Media + interactive | ⬜ Not started | Image + button round-trips work; signed URLs gated by auth + tenancy |
| Phase 7 — Production deploy | ⬜ Not started | systemctl restart whatsapp-mcp brings full stack back; /health 200 from open internet |
| Phase 8 — Future | ⬜ Deferred | Out of scope for v1 — sketches only |
Status legend: ⬜ Not started · 🟡 In progress · ✅ Done · ⏸ Blocked
To update: edit the Status column above as phases advance. Single source of truth for “where are we?”. The detailed checkboxes inside each phase file tell you what’s left; this table tells you which phase to look at.
Locked architectural decisions
| Area | Decision |
|---|---|
| WhatsApp API | Meta Cloud API, Graph v23.0 (bumped quarterly) |
| Tenant model | Single shared WABA number in v1; multi-tenant schema from day one |
| MCP transport | Remote Streamable HTTP/SSE + local stdio |
| Client auth | Per-client API keys (wamcp_live_...) with tool + number scopes |
| Background work | Inngest Cloud (orchestrator); functions execute inside the Node app |
| Datastore | PostgreSQL 16 via Docker Compose |
| Deployment | Docker Compose on a single Ubuntu host |
| Reverse proxy | Nginx + certbot (Let’s Encrypt) |
| Inbound → MCP | notifications/resources/updated push + get_messages pull backfill |
| v1 message types | Text (24h window), media (image/doc/audio), interactive (buttons/list). Templates deferred. |
| Media storage | Local disk /var/lib/whatsapp-mcp/media, signed-URL tool only |
| Audit + limits | Per-client audit log + per-key RPM + per-client daily message cap from v1 |
| Runtime | Node.js 20+, TypeScript strict, ESM, Drizzle ORM |
| Testing | Vitest (unit) + Vitest + testcontainers (integration); coverage gates per file group |
| Docs | Hand-written architecture + auto-generated TSDoc reference + per-component docs |
File index
Cross-cutting
| File | Purpose |
|---|---|
| architecture.md | Schema spine, auth pipeline, scope model, Inngest events, notification routing, secrets, env vars, threat model |
| standards.md | Engineering standards: mandatory tests, code commenting, TSDoc → markdown reference, docs structure |
Phases
| File | Effort | Goal |
|---|---|---|
| phase-0-meta-setup.md | S | External accounts, secrets, DNS, host baseline |
| phase-1-skeleton.md | M | TypeScript MCP server skeleton on stdio with Postgres + Drizzle |
| phase-2-outbound-and-webhook.md | L | End-to-end send + receive single-tenant |
| phase-3-inngest.md | L | Move side effects behind Inngest functions |
| phase-4-auth-audit.md | L | API keys, scopes, grants, rate limits, audit log |
| phase-5-remote-mcp.md | M | Streamable HTTP/SSE transport + notifications |
| phase-6-media-and-interactive.md | L | Media + interactive messages |
| phase-7-production-deploy.md | L | Docker Compose + Nginx + SOPS + backups |
| phase-8-future.md | deferred | Templates, multi-number ops, portal, scale-out, OpenTelemetry |
Ops runbook stubs
| File | Purpose |
|---|---|
| ops/client-onboarding.md | Onboard / offboard an MCP client |
| ops/phone-number-onboarding.md | Add a new WABA number to the server |
| ops/incident-runbook.md | Token rotation, DB restore, cert failure, pepper rotation |
| ops/upgrade.md | Graph API version bump procedure |
| ops/backups.md | Backup, restore, off-host sync verification |
Effort summary
v1 (Phases 0–7): roughly 4–6 focused engineering weeks for one experienced TypeScript developer. Meta verification waits overlap Phase 0–1.
Verification milestones (high level)
- After Phase 2 — stdio
send_message+ reply round-trip with rows inmessages. - After Phase 4 — second client with scoped key cannot exceed scope/grant/rate; every call audited.
- After Phase 5 — remote Claude on another machine receives
notifications/resources/updatedwithin ~1s; offline backfill viaget_messagesworks. - After Phase 7 —
systemctl restart whatsapp-mcpbrings the whole stack back;https://wa.<yourdomain>/healthreachable; restore from yesterday’spg_dumpsucceeds in staging compose.
How to use this plan
- Read architecture.md and standards.md before starting any phase. They are the contract everything else honours.
- Execute phases in order. Each phase file has explicit Tests and Acceptance sections — a phase is not done until both are green.
- Phase docs reference each other freely. Cross-cutting concerns (auth, Inngest events, schema) live in
architecture.md; phases describe what each one builds on top.