whatsapp-mcp
MCP server that controls WhatsApp Desktop via Chrome DevTools Protocol using the user's real session, enabling tools to list chats, read messages, search contacts, and send messages with simulated typing and rate limiting.
README
whatsapp-mcp
Servidor MCP que permite a opencode (o cualquier cliente MCP) controlar WhatsApp Desktop vía Chrome DevTools Protocol (CDP), usando la sesión real del usuario (misma cuenta, misma ventana, mismos datos) en lugar de una API no oficial. El control se hace sobre el DOM de web.whatsapp.com que renderiza el propio WhatsApp Desktop, con medidas anti-ban (rate limiting y typing simulado) para minimizar el riesgo de bloqueo de la cuenta.
Arquitectura
┌────────────┐ JSON-RPC 2.0 (stdio) ┌──────────────────────────────┐
│ opencode │ ◀──────────────────────▶ │ MCP server │
│ (cliente) │ │ src/server.ts │
└────────────┘ │ (10 tools expuestas) │
└───────────────┬───────────────┘
│ CDP (WebSocket)
▼
┌──────────────────────────────────┐
│ WhatsApp Desktop (Electron) │
│ --remote-debugging-port=9222 │
│ web.whatsapp.com │
└──────────────────────────────────┘
Capas del proyecto:
| Capa | Archivos | Responsabilidad |
|---|---|---|
| Config | src/config.ts |
Configuración con defaults y override por env (WA_MCP_*); objeto inmutable (Object.freeze). |
| CDP | src/cdp/ |
Cliente Chrome DevTools Protocol: descubrimiento de targets (HTTP /json), conexión WebSocket al target de WhatsApp, Runtime.evaluate y captura de pantalla. Implementado sobre el WebSocket nativo de Node (sin dependencias de runtime extra). |
src/whatsapp/ |
Operaciones de negocio sobre el DOM de WhatsApp: chats.ts (listar, no-leídos, buscar), messages.ts (leer mensajes), send.ts (enviar con typing simulado), draft.ts (leer/limpiar texto sin enviar del input), media.ts (descargar media de un mensaje a disco), dom.ts (selectores y extractores del DOM) y errors.ts (errores tipados). |
|
| Rate limit | src/ratelimit.ts |
RateLimiter anti-ban en memoria con ventana deslizante (3 políticas). Desacoplado del CDP para poder probarse sin WhatsApp; cubierto por tests unitarios (npm test, test/ratelimit.test.ts). |
| Server | src/server.ts |
Registra las 10 tools MCP, traduce errores a resultados estructurados { ok:false, error, message, retryAfterMs?, detail? } y gestiona el ciclo de vida (conexión on-demand, cierre limpio en SIGINT/SIGTERM). |
Flujo de una llamada típica: el server recibe tools/call por stdio → la tool correspondiente conecta (si no está cacheada) al target de WhatsApp vía CDP → evalúa JavaScript en la página → devuelve el resultado estructurado al cliente.
Requisitos
- Node.js >= 23 (se usa el type stripping nativo de TypeScript, sin build step; habilitado por defecto desde Node 23.6 — se probó con Node 24).
- WhatsApp Desktop instalado (ver Compatibilidad).
- Cliente MCP (p. ej. opencode) para consumir las tools.
Compatibilidad (¿cuál WhatsApp funciona?)
El server no usa una API no oficial: controla el DOM de web.whatsapp.com que renderiza la app de escritorio de WhatsApp. Por lo tanto, lo que necesita es una app de escritorio basada en Electron que muestre la web de WhatsApp.
- ✅ Probado y verificado en vivo: paquete AUR
whatsapp-linux-desktop-bin(la app no oficial de WhatsApp para Linux), versión 1.0.1-1, con binario en/opt/WhatsApp Desktop/whatsapp-linux-desktop. Es la que el wrapperscripts/launch-whatsapp.shlanza por defecto. - Los selectores del DOM (
src/whatsapp/dom.ts) y los mecanismos CDP fueron verificados contra el build 2026 de esa app (Electron 32). Si WhatsApp actualiza su web y cambia el DOM, hay que actualizar los selectores (ver Notas / limitaciones). - ⚠️ Puede funcionar con otras apps de escritorio de WhatsApp (WebCord, Ferdium, etc.) siempre que: (1) sean Electron y expongan
--remote-debugging-port, y (2) rendericenweb.whatsapp.comcon el mismo DOM. No están soportadas ni probadas: ajustaBINenscripts/launch-whatsapp.shy verifica los selectores antes de usarlas. - ❌ No funciona con WhatsApp Web en un navegador normal (necesitas el flag de CDP de un runtime controlable) ni con el cliente móvil.
Para saber qué tienes instalado:
pacman -Q | grep -i whatsapp # paquete + versión (p. ej. whatsapp-linux-desktop-bin 1.0.1-1)
ls /opt/ | grep -i whatsapp # binario (p. ej. "WhatsApp Desktop")
Instalación
npm install
No hay step de compilación: node src/server.ts ejecuta el TypeScript directamente.
Configuración
Todas las variables son opcionales y se leen del entorno con prefijo WA_MCP_. Ver .env.example.
| Variable | Default | Descripción |
|---|---|---|
WA_MCP_CDP_PORT |
9222 |
Puerto TCP donde Chrome/Electron expone el endpoint CDP. |
WA_MCP_CDP_HOST |
127.0.0.1 |
Interfaz donde escucha el endpoint CDP. |
WA_MCP_MEDIA_DIR |
/tmp/opencode |
Directorio donde se escriben los medios descargados (imágenes, documentos, etc.). |
WA_MCP_RATE_MIN_INTERVAL_MS |
3000 |
Delay mínimo entre dos send_message cualquiera (global). |
WA_MCP_RATE_COOLDOWN_CHAT_MS |
15000 |
Delay mínimo entre dos mensajes al mismo chat. |
WA_MCP_RATE_MAX_PER_MINUTE |
10 |
Tope de mensajes por ventana deslizante de 60s (todos los chats). |
WA_MCP_TYPING_ENABLED |
true |
Toggle del typing simulado. |
WA_MCP_TYPING_MIN_DELAY_MS |
40 |
Delay mínimo entre caracteres al teclear. |
WA_MCP_TYPING_MAX_DELAY_MS |
120 |
Delay máximo entre caracteres al teclear. |
WA_MCP_TYPING_PUNCTUATION_PAUSE_MS |
350 |
Pausa extra tras puntuación (. , ; : ! ? y salto de línea), con jitter 60–140%. |
WA_MCP_TYPING_THINK_BEFORE_SEND_MS |
600 |
Delay aleatorio (jitter 50–150%) entre terminar de teclear y pulsar enviar. |
WA_MCP_TYPING_MAX_MESSAGE_CHARS |
400 |
Mensajes más largos que esto omiten el typing simulado (inserción directa). |
Uso con WhatsApp
Lanzar WhatsApp con CDP
El server solo puede controlar WhatsApp si la app corre con el flag de debugging de CDP. El wrapper scripts/launch-whatsapp.sh lo garantiza de forma idempotente:
scripts/launch-whatsapp.sh # lanza (o reutiliza) WhatsApp con CDP en 127.0.0.1:9222
scripts/launch-whatsapp.sh --check # dry-run: solo informa qué haría, sin tocar nada
Tres casos que maneja el wrapper:
- Ya corriendo con el flag (
--remote-debugging-port=9222) → no hace nada; solo verifica que el endpoint CDP responda. - Corriendo sin el flag → termina esa instancia (SIGTERM → SIGKILL si es necesario), espera a que liberen los procesos hijos y relanza con el flag.
- No corriendo → lo lanza directamente con el flag.
El wrapper usa --no-sandbox (la app no tiene chrome-sandbox setuid-root) y comprueba el endpoint CDP durante 15s tras lanzar. El login/sesión persiste en el user-data-dir, así que relanzar es seguro.
IMPORTANTE: el server solo funciona con WhatsApp abierto y con la sesión iniciada. Si WhatsApp no está corriendo, las tools no crashean: devuelven un error estructurado accionable (
whatsapp_not_running) indicando cómo lanzarlo.
Override del .desktop de usuario
Para que WhatsApp siempre arranque con CDP (aunque se lance desde el menú, no solo desde el wrapper), el lanzador de aplicaciones del usuario (~/.local/share/applications/whatsapp-linux-desktop.desktop) apunta al wrapper:
Exec=/home/junior/Projects/whatsapp-mcp/scripts/launch-whatsapp.sh %U
El archivo fuente está en desktop/whatsapp-linux-desktop.desktop. Así cualquier apertura de WhatsApp pasa por el wrapper y garantiza el puerto CDP.
Integración con opencode
Registra el server como MCP local (stdio) en ~/.config/opencode/opencode.json:
{
"mcp": {
"whatsapp": {
"type": "local",
"command": ["node", "/home/junior/Projects/whatsapp-mcp/src/server.ts"],
"enabled": true
}
}
}
Después de editar el archivo hay que reiniciar opencode. Y ojo: el server MCP es un proceso stdio long-running, así que cualquier cambio bajo src/*.ts (no solo la config) tampoco se recarga en vivo — npm run verify y los tests spawnean instancias frescas y pasan con el código nuevo, pero el cliente opencode en ejecución conserva el código viejo hasta el reinicio. Para verificar que quedó registrado, revisa que las tools whatsapp_status, list_chats, read_messages, get_unread, search_contacts, read_draft, clear_draft, send_message, take_screenshot y download_media estén disponibles para el agente.
Leer adjuntos de WhatsApp desde opencode:
read_messagesreporta para cada mensaje suidytype. Para leer el contenido de un adjunto, llama adownload_mediacon eseid(la media se escribe en/tmp/opencode, o el directorio deWA_MCP_MEDIA_DIR) y luego lee elpathdevuelto con las herramientas de archivo de opencode (o el agentefile-analyser) para analizar la imagen, PDF, documento, etc.
Nota:
send_messageenvía mensajes reales. Configura los permisos de opencode (o el flujo de aprobación de tools) si quieres que cada envío requiera confirmación.
Tools MCP
| Tool | Argumentos | Descripción |
|---|---|---|
whatsapp_status |
— | Estado de WhatsApp: running, targetUrl, loggedIn y mensaje accionable. Nunca falla (es el health check). |
list_chats |
limit (default 20) |
Lista los chats renderizados en el panel (nombre, último mensaje, hora, no-leídos). |
read_messages |
chat (obligatorio), limit (default 20) |
Abre el chat indicado y lee sus últimos mensajes (autor, texto, hora/fecha, dirección, tipo). |
get_unread |
— | Chats con al menos un mensaje sin leer (nombre, último mensaje, contador). |
search_contacts |
query (obligatorio) |
Busca chats usando el buscador real de WhatsApp (no un filtro local). |
read_draft |
chat (obligatorio) |
Lee el draft (texto a medio escribir) del input del chat. Devuelve draft (texto) o null si el input está vacío. Solo lectura: no modifica nada. |
clear_draft |
chat (obligatorio) |
Limpia el draft del input. DESTRUCTIVO: elimina el texto sin enviar del usuario; devuelve el texto eliminado (previousDraft). Usar solo con aprobación explícita. |
send_message |
chat, text (obligatorios), clearDraft (opcional, default false) |
Envía un mensaje con typing simulado y pasando por el rate limiter. Si el chat tiene un draft, aborta con draft_conflict (sin tocar el draft) salvo que se pase clearDraft: true. Devuelve delivered, sentAt, chatId, preview. |
download_media |
chat, messageId (obligatorios), destDir (opcional) |
Descarga la media (imagen, video, audio, documento) del mensaje messageId (el data-id que reporta read_messages) y la escribe en destDir (default WA_MCP_MEDIA_DIR). Solo lectura: no envía mensajes ni pasa por el rate limiter. Devuelve path absoluto, filename, mimeType, sizeBytes, mediaType. Requiere que el chat esté abierto y el mensaje renderizado (la media puede no estar cargada si salió del viewport). |
take_screenshot |
— | Captura PNG de la ventana de WhatsApp vía CDP y la devuelve como data URL base64. |
Errores: todas las tools (salvo whatsapp_status) devuelven { ok:false, error, message, ... } con isError: true ante fallos, con claves estables (whatsapp_not_running, not_logged_in, chat_not_found, rate_limited con retryAfterMs, draft_conflict, send_not_confirmed, message_not_found, media_unsupported, media_not_loaded, cdp_error, unexpected).
Seguridad anti-ban
- Rate limiting (
src/ratelimit.ts): ventana deslizante de 60s con tres políticas evaluadas en orden antes de tocar el DOM — intervalo mínimo global (minIntervalMs), cooldown por chat (cooldownPerChatMs) y tope por minuto (maxPerMinute). Un envío bloqueado devuelverate_limitedconretryAfterMs. Nada se envía sin pasar porcheckSend. - Typing simulado: el texto se ingresa carácter a carácter con delays aleatorios configurables y pausas tras puntuación, más un delay de "pensar" antes de pulsar enviar. El indicador "escribiendo…" aparece para el receptor. Se omite (inserción directa, con
console.warn) cuando el mensaje superaWA_MCP_TYPING_MAX_MESSAGE_CHARSo el toggle está apagado. - Manejo de drafts (texto sin enviar): si el chat tiene un draft en el input,
send_messageaborta condraft_conflict(incluye el texto del draft en el mensaje) en lugar de borrarlo o concatenarlo con el mensaje. El draft nunca se sobreescribe sin consentimiento: solo se limpia conclear_drafto pasandoclearDraft: trueexplícitamente asend_message. - Sin broadcasts ni reenvíos automáticos: no hay código que haga envíos masivos.
- Confirmación de permisos:
send_messagees una tool como las demás; puede exigirse aprobación manual desde el cliente MCP (permissions de opencode).
Validación
npm run typecheck # tsc --noEmit (validación de tipos)
npm test # node:test (44 tests: RateLimiter, confirmación de envío, open-chat, media)
node scripts/verify.mjs # batería completa de verificación
npm run verify # alias del anterior
scripts/verify.mjs ejecuta y reporta PASS/FAIL/SKIP por sección:
- ENV: versión de Node (>= 23) y alcance del endpoint CDP.
- TYPECHECK:
npx tsc --noEmit. - MCP: spawn del server, handshake (
initialize+notifications/initialized),tools/list(las 10 tools),whatsapp_status,list_chats,get_unread,take_screenshoty cierre limpio con SIGTERM.send_messagenunca se invoca (solo se comprueba que esté registrada); lo mismo paradownload_media(presencia entools/listes suficiente).
Exit code 0 si no hay FAILs, 1 si algo falla. Si WhatsApp no está corriendo, los checks dependientes de CDP se reportan como SKIP (no FAIL) con un mensaje claro:
WA_MCP_CDP_PORT=9299 node scripts/verify.mjs # simula "WhatsApp caído"
Troubleshooting
"Sesión nueva / QR al lanzar"
WhatsApp Desktop no tiene single-instance lock: si arranca una segunda instancia (p. ej. la abres del menú estando ya abierta, o el wrapper relanza), dos procesos compiten por los mismos LevelDB y la segunda cae a un estado vacío (pantalla de QR/sesión nueva).
Solución: cerrar WhatsApp por completo y lanzarlo solo con el wrapper (una única instancia con el flag CDP):
scripts/launch-whatsapp.sh
El wrapper ya contempla el caso b (instancia sin flag → la termina y relanza). Evita abrir WhatsApp de cualquier otra forma mientras uses este server.
"Puerto 9222 no responde"
Las tools devuelven whatsapp_not_running. Causa casi siempre: WhatsApp no está corriendo con el flag de CDP. Lánzalo con el wrapper y verifica:
scripts/launch-whatsapp.sh
curl http://127.0.0.1:9222/json/version # debe responder con el "Browser" de WhatsApp
Errores comunes de las tools
| Error | Significado / solución |
|---|---|
whatsapp_not_running |
WhatsApp no está con CDP. Lanza con scripts/launch-whatsapp.sh. |
not_logged_in |
WhatsApp abierto pero en pantalla de QR/login. Completa el login en la ventana y reintenta. |
chat_not_found |
El nombre no coincide con el del chat list. Usa list_chats para ver los nombres exactos (incluyen emojis). Ten en cuenta que los emojis del nombre pueden CAMBIAR con el tiempo — matchea por el nombre base; list_chats/search_contacts devuelven el render actual. |
rate_limited |
Envío bloqueado por el rate limiter; respeta retryAfterMs. |
draft_conflict |
El chat tiene un draft (texto sin enviar) en el input. Usa read_draft para verlo, clear_draft o send_message con clearDraft: true para sobreescribirlo. |
input_not_found / send_not_confirmed |
El chat no cargó o el mensaje no se confirmó dentro del timeout. Reintenta. |
message_not_found |
download_media: el messageId no está en el DOM — el mensaje salió del viewport o el id es incorrecto. Re-pasa read_messages o scrollea el mensaje a la vista y reintenta. |
media_not_loaded |
download_media: se detectó media pero el blob no está disponible (blob URL revocado por virtualización) o el fetch falló. Reintenta con el chat abierto y el mensaje en el viewport. |
media_unsupported |
download_media: el mensaje no tiene media descargable (texto/unknown, o un documento que en este build no expone URL del archivo en el DOM). |
cdp_error / unexpected |
Problema de conexión o error inesperado; revisa los logs y verifica que WhatsApp sigue corriendo. |
Notas / limitaciones
- Selectores del DOM: las tools dependen de la estructura del DOM de WhatsApp Web, que cambia con las actualizaciones. Los selectores actuales fueron verificados contra el build 2026 (Electron 32); si WhatsApp cambia su DOM, habrá que actualizar
src/whatsapp/dom.ts,src/whatsapp/send.tsysrc/whatsapp/media.ts. - Descarga de media:
download_medianecesita que el mensaje esté renderizado en el DOM (WhatsApp virtualiza#main). El blob URL de la media se revoca si el mensaje sale del viewport, así que extrae con el chat abierto y, si falla conmedia_not_loaded/message_not_found, scrollea el mensaje a la vista y reintenta. Los documentos no exponen URL en el DOM en este build, así que no son descargables (media_unsupported/sin fuente). - Identificador de chat: la clave es el nombre del chat tal como aparece en el chat list (el DOM no expone un JID estable para todas las operaciones).
send_messageintenta leer el JID del header cuando está disponible, pero la identificación sigue siendo por nombre. Las variantes de emoji del mismo nombre se tratan como el mismo chat: la verificación del header (headerMatchesName, emoji-strip + case-insensitive) hace que un nombre cuya emoji difiera del header abra correctamente el chat. - Typing simulado omitido en mensajes largos: por encima de
WA_MCP_TYPING_MAX_MESSAGE_CHARSel texto se inserta de golpe (con aviso en stderr), para no bloquear el envío con un tecleo interminable. - Conexión on-demand: el server arranca sin tocar CDP y cada tool conecta/reutiliza la conexión WebSocket cacheada; se re-conecta si WhatsApp se relanza.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.