waha-mcp
Provides read-only access to WhatsApp groups and direct chats via WAHA, enabling listing groups and chats, reading messages and threads, fetching media, analyzing activity and pending replies, searching messages, and listing shared files.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
waha-mcp is a read-only MCP server over WAHA (the WhatsApp HTTP API). It lets Claude, ChatGPT and Gemini CLI analyze your WhatsApp groups and direct chats: summaries, activity stats, pending replies, search, shared files and media.
Nothing can be sent, edited or deleted: the WAHA client only issues GET requests and no tool writes anything.
What you can ask
"Who is waiting for my answer since yesterday?"
"Summarize what happened in <group> this week."
"Find the PDF Ana sent me last week."
"What does the voice note Ana sent this morning say?" (needs transcription)
"Turn my pending chats into Todoist tasks."
Related MCP server: WhatsApp MCP Server
How it works
flowchart LR
Client["Client<br/>(Claude / ChatGPT / Gemini CLI)"] -->|"OAuth 2.1 + MCP /mcp"| MCP["waha-mcp"]
MCP -->|"GET only"| WAHA["WAHA (GOWS)"]
WAHA --> WA["WhatsApp"]
MCP -.-> Data[("/data<br/>auth.db, cache.db, media secret")]Requirements
A running WAHA with the GOWS engine. WEBJS and NOWEB are not supported in v1: the server still starts, detects the engine and the tools report that it is unsupported.
Node 24 (the Docker image already includes it) and a public HTTPS URL for the server.
Quick start
Have WAHA running with the GOWS engine and a working session.
Run the server (replace the values):
docker build -t waha-mcp . docker run -d --name waha-mcp -p 3000:3000 \ -e WAHA_API_URL=http://<waha-host>:3000 \ -e WAHA_API_KEY=<your-waha-api-key> \ -e PUBLIC_BASE_URL=https://<your-public-domain> \ -v waha-mcp-data:/data \ waha-mcpPut it behind HTTPS at
PUBLIC_BASE_URL(a reverse proxy such as Traefik) and check<base>/health.Add
<base>/mcpto your client and log in with your WAHA API key. See Connect your client.
Tools
| Tool | What it does |
Discover | ||
๐ฅ |
| Lists your groups. |
๐ฌ |
| Lists direct (1:1) chats. |
โน๏ธ |
| Group details and participants. |
๐ง |
| Identifies a person by id, phone or name: names, shared groups, last interaction. |
Read | ||
๐ |
| Reads messages of a chat, newest first, with cursor pagination. |
๐งต |
| The conversation around a message. |
๐ |
| Fetches an attachment by |
๐๏ธ |
| Transcribes an audio or voice note by |
Analyze | ||
๐ |
| Counts, rankings, activity and response times for a chat in a period. |
โณ |
| Who is waiting for your answer, longest first. |
๐๏ธ |
| Activity across chats for a day or period. |
Find | ||
๐ |
| Case- and accent-insensitive search in one chat or across chats. |
๐ |
| Documents, media and links shared in chats, with a kind filter. |
Prompts
Prompt | What it does |
| Reviews the day's pending chats. |
| Turns pending replies into to-do tasks. |
| Summarizes a group. |
Message cache
Enabled by default (CACHE_ENABLED=true). The server keeps a SQLite copy of the messages it reads in DATA_DIR/cache.db, so repeated questions about a chat do not re-read its whole history from WAHA. A chat is cached the first time a tool reads it. Later reads ask WAHA only for messages newer than (newest cached message minus CACHE_REFRESH_HOURS), and a chat read less than 60 s ago is answered without calling WAHA. CACHE_MAX_AGE_DAYS prunes old messages (0 keeps them forever). Media files are never cached. Set CACHE_ENABLED=false to turn it off.
Sizing: every message read is kept, so on a busy account cache.db grows without limit by default. Set a retention such as CACHE_MAX_AGE_DAYS=90 there; older messages are simply read from WAHA again when asked for.
The cache is a copy of your conversations at rest on the server (cache.db, 0600). It is wiped when the server sees a different WhatsApp account after the next restart (the account is checked once per process, so restart the server after re-linking WAHA to another number), but logout does not clear it: delete cache.db (or set CACHE_ENABLED=false) to drop it. Edits and deletions older than the refresh window are not picked up, so a deleted message can stay cached.
Voice-note transcription (optional)
Off by default. Set TRANSCRIPTION_API_URL, TRANSCRIPTION_API_KEY and TRANSCRIPTION_MODEL together (setting only some of them is a startup error) and any OpenAI-compatible speech-to-text API works. For example, with OpenRouter:
TRANSCRIPTION_API_URL=https://openrouter.ai/api/v1
TRANSCRIPTION_API_KEY=sk-or-...
TRANSCRIPTION_MODEL=openai/whisper-large-v3-turbo
# optional language hint (ISO 639 code); omit to auto-detect
TRANSCRIPTION_LANGUAGE=esOpenAI and Groq endpoints also work (the server calls POST {TRANSCRIPTION_API_URL}/audio/transcriptions). When on, voice notes returned by get_messages, get_thread, list_pending_replies and get_activity_digest carry a transcript, search_messages also matches stored transcripts, and transcribe_audio transcribes any audio or voice note on demand. Automatic transcription is limited by TRANSCRIPTION_MAX_SECONDS and TRANSCRIPTION_MAX_PER_CALL; coverage notes say which voice notes were skipped (TRANSCRIPTION_MAX_PER_CALL=0 leaves everything to transcribe_audio). Each transcript is stored in cache.db, also when CACHE_ENABLED=false, so it is never paid twice. Transcripts are not pruned by CACHE_MAX_AGE_DAYS and stay after their message is deleted in WhatsApp; they are wiped with the rest of cache.db when the account changes (after the next restart), or when you delete the file.
New automatic transcriptions stop starting after 25 s within one tool call so answers stay fast; the coverage note lists the ones "not transcribed yet (time budget)", and the next call picks them up. Each failure is logged as one line transcription failed status=<code> kind=<kind> (never the audio, the URL or the key). kind=auth means the key was rejected, kind=config (HTTP 400/404/405/422) a wrong TRANSCRIPTION_API_URL or TRANSCRIPTION_MODEL, kind=rateLimited a provider limit; the coverage note names the same kind.
With transcription on, the audio of your voice notes is sent to the configured provider (a third party such as OpenRouter). Check its data policy before enabling it.
Files and PDFs
Only small images and stickers come back inline. Claude does not accept PDFs or other binary files from MCP connectors, and the model cannot open the signed link itself. For documents, PDFs, video and large images, get_media returns a short-lived link together with an instruction: the model gives you the link and asks you to download the file and attach it to the chat, so it can read it there. Audio is different: with transcription on, the model uses transcribe_audio; with it off, the model gives you the link so you can listen to it (Claude does not read attached audio). The server does not extract text from PDF or Word files.
Configuration
All configuration is through environment variables.
Variable | Required | Default | Description |
| yes | Base URL of WAHA (internal URL when deployed next to it). | |
| yes | WAHA API key. Also the OAuth login secret. | |
| yes | Public HTTPS URL of this server ( | |
| no |
| WAHA session name. |
| no |
| OAuth state ( |
| no |
| HTTP port. |
| no |
| IANA time zone for dates and day boundaries ( |
| no |
| Per-chat message cap; fan-out tools read up to 100 chats each. |
| no |
| Lifetime of signed media links. |
| no |
| Largest file returned inline (5 MiB); larger ones get a link. |
| no |
| Largest file served by |
| no |
| Keep a SQLite copy of read messages in |
| no |
| Each sync re-reads this window before the newest cached message. |
| no |
| Prune cached messages older than N days ( |
| no | OpenAI-compatible speech-to-text base URL ( | |
| no | API key of that provider. | |
| no | Model id ( | |
| no | Language hint, lowercase ISO 639-1/639-2 code (e.g. | |
| no |
| Longer voice notes are not transcribed automatically. |
| no |
| New automatic transcriptions per tool call ( |
Transcription is off unless TRANSCRIPTION_API_URL, TRANSCRIPTION_API_KEY and TRANSCRIPTION_MODEL are all set (setting only some of them is a startup error). The server calls POST {TRANSCRIPTION_API_URL}/audio/transcriptions with OpenAI-style multipart (file, model, language), which OpenRouter, OpenAI and Groq accept. When on, voice notes in get_messages, get_thread, list_pending_replies and get_activity_digest get a transcript (within the limits above; coverage notes report the skipped ones), search_messages also matches stored transcripts, and each transcript is stored in cache.db so it is never paid twice.
Deploy on EasyPanel
Create a service in the same project as WAHA, from this repository (Dockerfile build) or from a published image.
Set
WAHA_API_URLto the internal service URL,http://<project>_<service>:3000, so traffic never leaves the project network. SetWAHA_API_KEY,PUBLIC_BASE_URL(the domain you assign in step 3) and the optional variables above.Give only this service a public HTTPS domain. WAHA stays private.
Mount a volume at
/data, so logins and media links survive redeploys. If/datais a root-owned bind mount, runchown -R 1000:1000on it (the container runs as thenodeuser).Check
https://<your-domain>/health; it answers{"status":"ok"}.
The Docker healthcheck probes /health on port 3000; if you change PORT, adjust or disable it.
Connect your client
Let <base> be your PUBLIC_BASE_URL. On the login page, paste your WAHA API key.
claude.ai: Settings > Connectors > add custom connector, URL
<base>/mcp; complete the OAuth login.Claude Code:
claude mcp add --transport http waha <base>/mcp, then authenticate from/mcp.ChatGPT: enable Developer Mode, add a connector with URL
<base>/mcpand OAuth authentication.Gemini CLI: in
~/.gemini/settings.json:{ "mcpServers": { "waha": { "httpUrl": "<base>/mcp" } } }then authenticate with
/mcp auth waha.
To sign out everywhere, open <base>/logout and paste the key: it revokes every client and session.
Security notes
OAuth 2.1 with PKCE (S256). Tokens are opaque random values stored only as hashes.
Direct chats are private data. Use a client you trust with them.
The API key and WAHA URL are never logged or returned in errors.
Rotating
WAHA_API_KEYrevokes every issued token at the next start: only a hash of the key is kept inauth.dbto detect the change.auth.dbis created with0600permissions.The message cache is a copy of the conversations you read: see Message cache.
Transcription is off by default; when on, voice notes go to the configured provider: see Voice-note transcription.
TRANSCRIPTION_API_KEYis never logged or returned; provider errors are reported with fixed messages.At most 50 OAuth clients can be registered. Clients older than 24 h with no live token are pruned automatically when a new one registers;
<base>/logoutclears them all.
The login is rate limited per client address (IPv6 grouped by /64) and with a global failure budget. An attacker can therefore lock the owner out temporarily, and logout is also blocked during that window.
The client address is the last hop ofX-Forwarded-For, which is only trustworthy behind a reverse proxy that appends it (Traefik on EasyPanel). Do not expose the server directly to the internet.
Signed media links expire afterMEDIA_LINK_TTL_SECONDS and behave like bearer credentials until then: anyone holding the link can download that file.
Known limitations
GOWS does not return reactions or polls on the messages endpoint.
The chats list may omit some active chats; pending-reply and digest results say so in their coverage notes.
Search and digest work on the history WAHA still returns and the cache has already read; results report their coverage.
Dates are read in
TIMEZONE: a bare date (for example2026-10-01) means that local day (asto, through its last second), and a date-time without offset is read inTIMEZONE. A date-time withZor an offset is taken as given.Local phone numbers (without country code) are matched by their ending.
Development
pnpm install
pnpm test # vitest
pnpm typecheck
pnpm lint # biome
pnpm build # tsc to dist/
pnpm dev # run from source with .envpnpm capture -- --group <id> | --chat <id> captures GOWS payloads from a live WAHA (GET only) and writes them as test fixtures. A scrubber anonymizes names, numbers, ids and text before writing, so no real data is committed.
License
This server cannot be deployed
Maintenance
Related MCP Connectors
Your own WhatsApp as an MCP server: read, search and send from any MCP client.
Read-only WhatsApp MCP for your AI agent: it reads the chats you choose. Try the public demo.
Summarize, search, and manage your own WhatsApp groups from any MCP client.
Let Claude or ChatGPT search, read and send your WhatsApp messages over MCP. OAuth sign-in.
Related MCP Servers
- FlicenseNot gradedqualityNot gradedmaintenanceA self-hosted MCP server that connects AI clients to WhatsApp via the WAHA HTTP API. It enables users to manage sessions, search contacts, and send or receive messages and media directly through natural language interfaces.5 npm-
- AlicenseNot gradedqualityDmaintenanceGives Claude Desktop read-only access to your WhatsApp chats and message history via a local MCP server.6 npm3MIT
- FlicenseBqualityDmaintenanceRead-only MCP server for WhatsApp Web allowing listing chats and reading recent messages, without sending or mutating WhatsApp state.34 npm-
- AlicenseAqualityCmaintenanceMCP server that gives AI agents read and write access to WhatsApp on macOS, reading local chat history offline and sending messages via pre-filled drafts with explicit user confirmation.849 PyPIMIT