WhatsApp MCP

Eigener MCP-Server unter mcp-whatsapp fuer die WhatsApp Business Cloud API. Gebaut 2026-05-13 als zweite Branchen-Komponente fuer das Salon-Vertical (zusammen mit calcom).

Pattern: Outbound-Tools rufen Meta Graph API. Eingehende Nachrichten kommen ueber Webhook → werden im integrierten POST /webhook-Endpoint angenommen + in lokaler SQLite gespeichert. Read-Tools lesen aus SQLite. Das Brain (Claude Code Session oder spaeter Lambda) pollt list_recent_messages(only_unprocessed=True) und orchestriert.

Was kann der MCP

15 Tools — 13 dedizierte + 2 Raw-Escapes:

KategorieTools
Outboundsend_text, send_template, send_image, send_document, send_interactive_buttons, send_interactive_list
Statusmark_read (WhatsApp-Lesebestaetigung), mark_processed (lokal als verarbeitet)
Inboxlist_recent_messages, get_message
Phone + Templatesget_phone_info, list_phone_numbers, list_templates
Raw-Escapesraw_get, raw_post

Endpoints des Servers

EndpointZweck
POST /mcpMCP-Tool-Aufrufe (Standard streamable-http)
GET /webhookMeta-Verify-Handshake (vergleicht hub.verify_token mit WHATSAPP_WEBHOOK_VERIFY_TOKEN)
POST /webhookEingehende Messages → SQLite-Inbox
GET /healthLiveness

Setup

1. Meta-App + WhatsApp Cloud API einrichten

  1. https://developers.facebook.com/My AppsCreate App
  2. Use case: Other → Type: Business → Name agentic-friseur-bot, Email hello@marvinkuehlmann.com
  3. Im App-Dashboard → linke Sidebar → WhatsAppSet up. Bei Frage nach Business-Account: „Create a new business account” → Name Agentic Ventures
  4. API Setup-Tab oeffnet — drei Werte kopieren:
    • Phone number IDWHATSAPP_PHONE_NUMBER_ID
    • WhatsApp Business Account IDWHATSAPP_BUSINESS_ACCOUNT_ID
    • Temporary access tokenWHATSAPP_ACCESS_TOKEN (24h gueltig)
  5. Eigene Handynummer im „To”-Dropdown verifizieren — Meta schickt 6-stelligen Code via WhatsApp.

Fuer Permanent-Token: Meta Business Settings → System Users → User anlegen → Permanent-Token mit Scopes whatsapp_business_messaging + whatsapp_business_management. Sonst muss der temporaere alle 24h ersetzt werden.

2. Env

cp ~/source/mcps/mcp-whatsapp/.env.local.example ~/source/mcps/mcp-whatsapp/.env.local
# Werte eintragen

3. Install + Server starten

uv tool install --force --editable ~/source/mcps/mcp-whatsapp
mcp-whatsapp  # laeuft auf 127.0.0.1:8771

4. In Claude Code registrieren

claude mcp add whatsapp --transport http http://127.0.0.1:8771/mcp

5. Webhook bei Meta einrichten (fuer Inbound)

Server muss aussen erreichbar sein — ngrok-Tunnel:

ngrok http 8771
# kopiere die https-URL: https://<random>.ngrok-free.app

In Meta-App → WhatsApp → ConfigurationWebhook:

  • Callback URL: https://<random>.ngrok-free.app/webhook
  • Verify token: identisch mit WHATSAPP_WEBHOOK_VERIFY_TOKEN in .env.local
  • Subscribe to fields: messages, message_status

Meta sendet GET → unser Server antwortet mit hub.challenge wenn Token matcht → Webhook ist live. Ab jetzt kommen Inbound-Messages in die SQLite.

6. Smoke

# Nach Claude-Code-Reload:
mcp__whatsapp__get_phone_info()
# → {verified_name, display_phone_number, quality_rating, status}
 
mcp__whatsapp__send_text(to="491701234567", text="Hallo aus dem mcp-whatsapp!")
# Sollte auf dem verifizierten Handy ankommen.
 
# Wenn du auf die Test-Nummer zurueckschreibst (innerhalb 24h Service-Window):
mcp__whatsapp__list_recent_messages(only_unprocessed=True, limit=5)
# Sollte die Nachricht zeigen.

Quirks

  • Test-Phone-Number-Mode: maximal 5 verifizierte Empfaenger im Test-Modus. Empfaenger werden im API-Setup-Tab eingetragen.
  • 24h-Service-Window: Ausserhalb darf nur ein approved Template gesendet werden, kein Freitext. Innerhalb: Freitext OK. Templates fuer Bestaetigung + Erinnerung muessen vorher bei Meta eingereicht werden (Approval 1-3 Tage).
  • Webhook-Verify-Token: beim Meta-Setup EXAKT der Wert in .env.local. Sonst „verification failed”.
  • Inbound-Webhook braucht 2xx innerhalb ~20s — sonst retried Meta. SQLite-Insert ist schnell genug, daher kein async Queue.
  • Permanent-Token-Scopes: whatsapp_business_messaging (Senden) + whatsapp_business_management (Templates, Phone-Info). Ohne zweites Scope schlagen list_templates und list_phone_numbers fehl.
  • Interactive-Replies kommen als type=interactive in Webhook-Payload zurueck — interactive.button_reply.id bzw. interactive.list_reply.id enthaelt die geklickte Auswahl. Unser _store_incoming extrahiert die title als text_body fuer einfache Bot-Logik.
  • Webhook-Payload-Parsing-Robustheit: Wenn Meta einen Status-Update-Event statt einer Message schickt (z.B. Delivery-Receipt), kommt value.messages einfach leer zurueck — wir ignorieren das stillschweigend.
  • FastMCP DNS-Rebinding-Schutz blockt Tunnel-Host (421 Invalid Host header). Default-Allowlist akzeptiert nur 127.0.0.1:*/localhost:*/[::1]:*. Cloudflared reicht den Public-Host mcp-whatsapp.agenticventures.de durch → 421. Fix: Env-Var MCP_ALLOWED_HOSTS=mcp-whatsapp.agenticventures.de am Container (in infra/lib/mcp-whatsapp-hosted-stack.ts gesetzt). Server liest sie in main() und erweitert mcp.settings.transport_security.allowed_hosts + allowed_origins. Erstmals 2026-05-15 nach Deploy aufgeschlagen, gefixt durch CDK-Redeploy. Voller Pattern in quirks—gotchas.

Bot-Workflow (Friseur)

1. User schickt WhatsApp "Mittwoch 14 Uhr Damen-Schnitt frei?"
   → Meta Webhook → POST /webhook → SQLite-Insert
2. Brain (Claude Code / Lambda) pollt list_recent_messages(only_unprocessed=True)
3. Brain ruft mcp__whatsapp__mark_read(wamid)  // blauer Haken
4. Brain ruft mcp__calcom__list_slots(event_type_id=<damen-schnitt>, start, end)
5. Brain formuliert Antwort und ruft mcp__whatsapp__send_interactive_buttons(
     to=from_phone,
     body_text="Diese Slots sind frei: 13:30, 14:00, 14:30",
     buttons=[{id:"book-1330", title:"13:30"}, {id:"book-1400", title:"14:00"}, ...]
   )
6. User klickt Button → kommt als type=interactive zurueck mit button_reply.id="book-1400"
7. Brain ruft mcp__calcom__create_booking(...)
8. Brain ruft mcp__whatsapp__send_text("Termin gebucht. Bis Mittwoch 14:00!")
9. Brain ruft mcp__whatsapp__mark_processed(wamid)

Production-Hosting (live seit 2026-05-14)

Endpoint: https://mcp-whatsapp.agenticventures.de (MCP /mcp, Webhook /webhook, Health /health)

KomponenteWert
AWS-Accountav-production (425924867359)
Regioneu-central-1
ECS-Clusterdefault
ECS-ServiceMcpWhatsappHosted-ServiceD69D759B-HuB3n6EMF0qS
ECR-Repo425924867359.dkr.ecr.eu-central-1.amazonaws.com/mcp-whatsapp-hosted
TaskDef0.25 vCPU / 0.5 GB, 2 Container (whatsapp + cloudflared)
Cloudflare-Tunnela3af8b46-0236-4dd4-93da-e83f66c56f1b
DNS-Recordmcp-whatsapp.agenticventures.de<tunnel-id>.cfargotunnel.com proxied=true
Secrets Managermcp-whatsapp-hosted/whatsapp-config (JSON) + mcp-whatsapp-hosted/cloudflared-token
StackCDK in ~/source/mcps/mcp-whatsapp/infra/ (McpWhatsappHosted)

Pattern: mcp-hosting-fargate-tunnel. SQLite-Inbox ist ephemeral (geht bei Container-Restart verloren) — fuer Multi-Tenant-Live spaeter DynamoDB.

Production-Smoke (2026-05-14)

Voller End-to-End-Round-Trip mit Friseur-Account (889608426871476 / Phone 1170948102758111 / App 988192143605811):

  • Webhook msg from=4915128945607 type=text preview='Hi' → SQLite-Insert
  • send_text → Meta wamid → status=sent Webhook-Confirm
  • Service-Window-Regel bestaetigt: Outbound nur innerhalb 24h nachdem User zuerst geschrieben hat (sonst 131047 Re-engagement message)