Skip to main content
Glama

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

  1. Have WAHA running with the GOWS engine and a working session.

  2. 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-mcp
  3. Put it behind HTTPS at PUBLIC_BASE_URL (a reverse proxy such as Traefik) and check <base>/health.

  4. Add <base>/mcp to your client and log in with your WAHA API key. See Connect your client.

Tools

&nbsp;

Tool

What it does

Discover

๐Ÿ‘ฅ

list_groups

Lists your groups.

๐Ÿ’ฌ

list_chats

Lists direct (1:1) chats.

โ„น๏ธ

get_group_info

Group details and participants.

๐Ÿง‘

get_contact

Identifies a person by id, phone or name: names, shared groups, last interaction.

Read

๐Ÿ“œ

get_messages

Reads messages of a chat, newest first, with cursor pagination.

๐Ÿงต

get_thread

The conversation around a message.

๐Ÿ“Ž

get_media

Fetches an attachment by media_id (inline when small, otherwise a signed link).

๐ŸŽ™๏ธ

transcribe_audio

Transcribes an audio or voice note by media_id (optional, see below).

Analyze

๐Ÿ“Š

get_chat_stats

Counts, rankings, activity and response times for a chat in a period.

โณ

list_pending_replies

Who is waiting for your answer, longest first.

๐Ÿ—“๏ธ

get_activity_digest

Activity across chats for a day or period.

Find

๐Ÿ”Ž

search_messages

Case- and accent-insensitive search in one chat or across chats.

๐Ÿ“

list_shared_files

Documents, media and links shared in chats, with a kind filter.

Prompts

Prompt

What it does

daily-review

Reviews the day's pending chats.

pending-to-todoist

Turns pending replies into to-do tasks.

group-summary

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.

WARNING

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=es

OpenAI 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.

WARNING

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

WAHA_API_URL

yes

Base URL of WAHA (internal URL when deployed next to it).

WAHA_API_KEY

yes

WAHA API key. Also the OAuth login secret.

PUBLIC_BASE_URL

yes

Public HTTPS URL of this server (http only for localhost).

WAHA_SESSION

no

default

WAHA session name.

DATA_DIR

no

/data

OAuth state (auth.db), message cache (cache.db), media secret.

PORT

no

3000

HTTP port.

TIMEZONE

no

UTC

IANA time zone for dates and day boundaries (America/Lima).

STATS_MAX_MESSAGES

no

5000

Per-chat message cap; fan-out tools read up to 100 chats each.

MEDIA_LINK_TTL_SECONDS

no

600

Lifetime of signed media links.

MEDIA_INLINE_MAX_BYTES

no

5242880

Largest file returned inline (5 MiB); larger ones get a link.

MEDIA_MAX_BYTES

no

104857600

Largest file served by /media/:token (100 MiB).

CACHE_ENABLED

no

true

Keep a SQLite copy of read messages in DATA_DIR/cache.db.

CACHE_REFRESH_HOURS

no

48

Each sync re-reads this window before the newest cached message.

CACHE_MAX_AGE_DAYS

no

0

Prune cached messages older than N days (0 keeps them forever).

TRANSCRIPTION_API_URL

no

OpenAI-compatible speech-to-text base URL (https://openrouter.ai/api/v1).

TRANSCRIPTION_API_KEY

no

API key of that provider.

TRANSCRIPTION_MODEL

no

Model id (openai/whisper-large-v3-turbo).

TRANSCRIPTION_LANGUAGE

no

Language hint, lowercase ISO 639-1/639-2 code (e.g. es); omit to detect.

TRANSCRIPTION_MAX_SECONDS

no

300

Longer voice notes are not transcribed automatically.

TRANSCRIPTION_MAX_PER_CALL

no

20

New automatic transcriptions per tool call (0 = only transcribe_audio).

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

  1. Create a service in the same project as WAHA, from this repository (Dockerfile build) or from a published image.

  2. Set WAHA_API_URL to the internal service URL, http://<project>_<service>:3000, so traffic never leaves the project network. Set WAHA_API_KEY, PUBLIC_BASE_URL (the domain you assign in step 3) and the optional variables above.

  3. Give only this service a public HTTPS domain. WAHA stays private.

  4. Mount a volume at /data, so logins and media links survive redeploys. If /data is a root-owned bind mount, run chown -R 1000:1000 on it (the container runs as the node user).

  5. 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>/mcp and 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_KEY revokes every issued token at the next start: only a hash of the key is kept in auth.db to detect the change. auth.db is created with 0600 permissions.

  • 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_KEY is 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>/logout clears them all.

WARNING

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.

WARNING

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.

WARNING

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

NOTE

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 example 2026-10-01) means that local day (as to, through its last second), and a date-time without offset is read in TIMEZONE. A date-time with Z or 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 .env

pnpm 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

MIT


Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    Not graded
    maintenance
    A 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
    -
  • A
    license
    A
    quality
    C
    maintenance
    MCP 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.
    8
    49 PyPI
    MIT