whatsapp-mcp-server
Provides tools for interacting with WhatsApp, including searching and reading message history, sending messages and media, managing voice notes, and resolving cross-identity contacts.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@whatsapp-mcp-serversearch for messages from John about project update"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
whatsapp-mcp-server
A self-hosted, authenticated MCP server for WhatsApp: 96 tools over your own message history, with server-side tool retrieval so the model pays for 29 of them instead of 96, and a labeled eval that measures whether retrieval actually picks the right tool.
Runs as two containers on a small VPS. Your messages stay on your box; the only credential is a bearer token you generate.
Claude / any MCP client ──HTTPS + Bearer──> nginx ──> mcp container ──> bridge container ──> WhatsAppWhy this exists
The reference WhatsApp MCP servers are stdio-only, unauthenticated, and expose a flat tool list. That is fine on a laptop and unworkable as an always-on service. Three problems show up immediately, and most of this repo is the answer to them.
1. Tool definitions are a fixed tax. MCP injects every tool definition into the model's context on every request, whether or not the request touches WhatsApp. At 96 tools that is roughly 20k tokens you pay for asking "what time is it". Dropping tools to save tokens means losing capability.
2. The same person has two identities. WhatsApp hides phone numbers behind LIDs inside groups, so one human appears under their phone number in DMs and under an opaque LID in group history. Any tool that filters on a single sender ID silently returns half their messages, with no error to tell you.
3. The app's UI is not the protocol. "View once", "delete for everyone", poll selection caps: these are conventions the client draws, not guarantees the server enforces. A server that reads the wire will disagree with the app, and when it does, the app is usually the one that is wrong.
Related MCP server: WAHA MCP
The parts worth reading
Progressive disclosure: 96 tools, 29 tools' worth of context
WHATSAPP_MCP_TOOLSET=minimal (the default) serves 29 hot-core tools directly and
prunes the other 67 from the served set, but captures the full library first so
nothing becomes unreachable. Two meta-tools bridge the gap:
find_tool(query) -> ranked catalog entries (name, description, param signature)
call_tool(name, arguments) -> dispatch to any tool in the full 96-tool librarytools served | always-on cost | |
| 96 | ~20k tokens |
| 29 + 2 meta | ~8k tokens |
No capability is removed and there is no mode to flip at call time. The long tail is one search away.
The security consequence is the interesting part. call_tool dispatches
inside the process, past the MCP on_call_tool middleware chain, so it has to
re-apply everything that chain would have done: per-tool scope enforcement and
audit logging of mutating calls. Both are mirrored in toolsearch.py so the
dispatcher can never become a scope bypass or an audit hole. A retrieval layer
that quietly skips your authorization middleware is worse than no retrieval layer.
Retrieval is lexical by default (IDF-weighted, stemmed, with a synonym alias map) and upgrades to hybrid lexical + semantic if any embedding provider key is present, with no behaviour change when there is none.
The retrieval is measured, not asserted
tests/toolsearch-eval/ holds a labeled set of natural-language tasks with a gold
tool and acceptable alternates per case, plus a probe that dumps what find_tool
actually returns for each. Labels are deterministic, so recall@k needs no LLM
judge. Queries were written independently of the alias map, and the README there
says so, because an eval tuned against its own answer key measures nothing.
Lexical-only recall@8 caps around 75% on adversarial slang. That number is in the repo because a retrieval layer without a number attached is a vibe.
Cross-identity resolution
identity.py reads whatsmeow's own whatsmeow_lid_map and whatsmeow_contacts
tables, read-only, and joins a person's phone number and LID into one identity.
list_person_messages then returns their full history across DMs and groups
instead of whichever half matched.
Three properties it holds, in priority order:
Read-only. Never writes, never touches the message store.
Additive. No existing code path imports it; resolution is a lens a caller opts into, so existing tools cannot regress.
Never wrong. Anything unmapped resolves to itself. It can make attribution more accurate; it can never make it confidently wrong.
Documented attribution limits
Group messages before a certain date were stored with the group's JID as the sender. The real sender was never recorded and cannot be recovered. The server reports those as unattributable rather than inferring a sender from context.
Same posture in docs/protocol-vs-app.md, which
catalogues where the wire and the app disagree and marks every claim as
VERIFIED (tested here), SOURCE (read in the protocol) or SPECULATIVE (not
confirmed). Several capabilities in there are deliberately not implemented:
the wire permits them, and they work by deceiving a recipient rather than by
reading data already delivered to us. That line is the project's one editorial
position.
Voice notes are searchable text
transcription.py runs a background worker that finds voice notes with no
transcript, decrypts the audio, transcribes it through a hosted API, and stores
the result in a separate writable database. The message store is mounted
read-only and the bridge writes it with INSERT OR REPLACE, so a transcript
column on messages would be silently wiped on any resync.
Arabic gets its own normalisation pass: clitics (ال, و, ب, ل, ف) attach
as prefixes, so plain FTS token matching never matches فارماسي against a stored
الفارماسي. A folded copy of each transcript handles that plus the spelling
variants people type inconsistently (alef forms, ة/ه, ى/ي).
Architecture
┌─────────────────────────────────────────────┐
MCP client ──────>│ nginx (TLS, proxy_buffering off for /mcp) │
Bearer token └───────────────────┬─────────────────────────┘
│ 127.0.0.1:9100
┌───────────────────▼─────────────────────────┐
│ mcp container (Python, FastMCP) │
│ token verify + scopes observability.py │
│ 96 tools tools.py │
│ find_tool / call_tool toolsearch.py │
│ LID <-> phone identity.py │
│ CDN decrypt media.py │
│ voice -> text transcription.py │
│ send-later queue scheduling.py │
└───────────────────┬─────────────────────────┘
│ http://bridge:8080 (private network)
┌───────────────────▼─────────────────────────┐
│ bridge container (Go, whatsmeow) │
│ holds the WhatsApp session │
│ writes messages to SQLite (FTS5) │
│ REST API, never published publicly │
└─────────────────────────────────────────────┘The bridge is
expose:-only. It is reachable from the mcp container and from nowhere else.The mcp server publishes on
127.0.0.1only; nginx is the sole public path.Auth is enforced in the application, not just at the proxy, so a proxy misconfiguration cannot open the server.
Modules
File | Lines | What it does |
| ~5,800 | whatsmeow session, SQLite ingest, internal REST API |
| ~2,550 | Query layer over the message store |
| ~1,530 | The 86 general tools, with MCP annotations and clamps |
| ~630 | Tool retrieval, dispatch, scope + audit mirroring |
| ~580 | Voice-note worker, key failover, Arabic normalisation |
| ~520 | CDN fetch, HKDF + AES-CBC decrypt, signed download links |
| ~440 | App assembly, token/scope verification, toolset pruning |
| ~420 | Send-later queue, drafts, chat notes |
| ~240 | Prometheus metrics + append-only JSONL audit log |
| ~175 | LID <-> phone resolution |
| ~155 | Optional semantic retrieval, any of three providers |
| ~175 | MCP prompts and resources |
Tools
96 tools. In the default minimal toolset the 29 bold ones are served directly;
the rest are reachable through find_tool / call_tool.
Group | Tools |
Search / read |
|
Send |
|
Media |
|
Voice |
|
Polls |
|
Message ops |
|
Identity |
|
Scheduling |
|
Groups |
|
Chat state |
|
Analytics |
|
Profile / privacy |
|
Newsletters |
|
Ops |
|
Meta |
|
Read tools are cached for 30s per token. Mutating tools are never cached.
Destructive tools require an explicit confirm=True that the model must produce
on purpose, because older MCP clients do not support elicitation and an
annotation alone is only a hint to the client.
Quick start
Requires Docker, a domain with TLS, and a phone with WhatsApp.
git clone https://github.com/HalemoGPA/whatsapp-mcp-server.git
cd whatsapp-mcp-server
printf 'WHATSAPP_MCP_TOKEN=wamcp_%s\n' "$(openssl rand -hex 32)" > .env
chmod 600 .env
docker compose up -d --build
curl -fsS http://127.0.0.1:9100/health # {"ok": true}Pair the phone once. The QR refreshes about every 20 seconds, so be ready:
docker compose logs -f bridge # "Scan this QR code with your WhatsApp app:"WhatsApp → Settings → Linked Devices → Link a Device. The session persists in the
wa-store volume across restarts.
Then put nginx in front (deploy/nginx/whatsapp-mcp.conf) and connect a client:
claude mcp add -s user --transport http whatsapp https://your-host/mcp \
--header "Authorization: Bearer $WHATSAPP_MCP_TOKEN"Full deployment, rollback, and the OS-level safety stack:
docs/DEPLOY.md and deploy/ops/README.md.
Verify auth is actually on
# no token MUST be 401
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://your-host/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Configuration
Everything is environment variables. The only required one is the token.
Variable | Default | Purpose |
| (required) | The single owner bearer token |
| unset | Multiple tokens with per-token scopes |
|
|
|
|
| Extra dirs a send tool may read from (colon-separated); the built-in media dir is always allowed |
|
| Append-only audit trail (the compose file overrides this to |
|
| Semantic weight in hybrid retrieval |
| unset | Any one enables semantic retrieval |
| unset | Enable voice transcription |
|
| Transcription language |
|
| Downloaded-media reaper, |
| off | Per-message logging. Off keeps content out of logs |
Scopes
With WHATSAPP_MCP_TOKENS_JSON each token carries scopes, enforced per tool:
whatsapp:read · whatsapp:send · whatsapp:admin · whatsapp:full
A read-only token confines a summarising agent to reading and cannot send. The
enforcement applies to call_tool dispatch as well as to direct calls, so the
retrieval layer cannot be used to reach a tool the token's scope forbids.
Testing
pytest # 95 offline tests. No WhatsApp, no bridge, no network.
ruff check .
cd whatsapp-bridge && go vet -tags sqlite_fts5 ./...The offline suite covers identity resolution against a synthetic whatsmeow store and the retrieval invariants (stemming, scoring order, signature rendering, graceful degradation with no embeddings). It runs in CI on every push.
Three more suites need a live paired session and are run by hand:
Suite | What it checks |
| Every tool over real HTTP, including the 401 path |
| Retrieval recall@k against the labeled set |
| Transcription quality on real audio |
sqlite_fts5 is not optional for the bridge. Without the build tag it crash-loops
on the FTS5 virtual table.
Performance
Measured against the upstream project on the same 77k-message store.
SQLite indexes on
messages(chat_jid, timestamp DESC),messages(sender, timestamp DESC),messages(timestamp DESC),chats(last_message_time DESC). Read tools went from a full-table scan (200ms+) to index seeks (single-digit ms).FTS5 contentless mirror on
messages.contentreplacesLIKE '%foo%'. Content search is 12x to 3000x faster depending on selectivity.include_context=Truecollapsed from1 + 3Nqueries to one query per anchor, via a UNION ALL of ordered window slices.History sync batched into transactions every 500 rows: 10x to 50x faster ingest on a fresh pair.
Per-thread SQLite connections with PRAGMAs applied once per thread rather than per call (
mmap_size=128MiB,query_only,temp_store=MEMORY).WAL bounded by a 5-minute
wal_checkpoint(TRUNCATE), so a long-lived read-only reader cannot let the WAL grow without limit.Images: bridge 201MB to 55MB (distroless,
-trimpath,-ldflags="-s -w"), mcp 1.05GB to 509MB (multi-stage uv with a frozen lockfile, static ffmpeg).
Security model
One credential. A bearer token in .env, verified in the application. No
token or a wrong token is a 401 before any tool runs.
The bridge is not public. It is on a private Docker network with no published port. The mcp server binds loopback only.
Send paths are sandboxed. media_path is confined to WHATSAPP_MEDIA_ROOTS,
so a prompt-injected model cannot ask the bridge to send messages.db to a
contact.
Inputs are clamped server-side: limit <= 100, context windows <= 20,
message body <= 65536 bytes, media <= 64MB with a 1MB JSON body cap.
Mutating calls are audited to an append-only JSONL log, fsynced per line, and
calls routed through call_tool are tagged as such.
Rate limited at the bridge (5/s, burst 10) so a runaway agent cannot trip a WhatsApp temporary ban.
Least privilege on the host. deploy/sudoers.d/mcp-ops grants exactly the
commands the deploy needs and documents why each omission is an omission:
systemctl restart nginx is absent because the project rule is reload-only;
apt install is absent because it is too broad.
What this does not protect you from
Message content goes to your model provider when you use the tools. Self-hosted storage is not the same as private from the model. The decrypted history lives in a Docker volume on your box; keep it out of any shared backup.
See SECURITY.md for the threat model and how to report an issue.
Attribution
Originally forked from lharries/whatsapp-mcp
(MIT), which supplied the whatsmeow bridge skeleton and the first read tools.
Both halves have since been substantially rewritten. Of the 14 files in the
upstream tree, one (whatsapp-mcp-server/audio.py) is carried unchanged; the Go
bridge grew about 5x and the Python server about 4x, and everything under
toolsearch.py, transcription.py, media.py, scheduling.py,
observability.py, identity.py, embeddings.py, prompts.py, resources.py,
deploy/ and tests/ is new here.
Substantive changes to the inherited code:
whatsmeow updated. Upstream pinned a version WhatsApp now rejects with
405 client outdated.context.Contextthreaded through the five whatsmeow calls that now require it, and propagated per request so cancellation reaches uploads.Fresh-store nil-device fix. The current
GetFirstDevicereturns(nil, nil)for an empty store; without a nil check the bridge never shows a QR code.stdio to authenticated Streamable HTTP. Upstream was stdio with no auth.
Paths and binds are environment-driven rather than hardcoded.
License
MIT. See LICENSE, which carries both copyright lines.
Contributing
See CONTRIBUTING.md. Short version: this repo's one rule is in CLAUDE.md, and it is that a rendering is evidence of what a client chose to draw, and nothing more. Cite the wire, the proto field, or a test you ran.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- FlicenseAqualityDmaintenanceAn MCP server that wraps the wacli tool to enable AI clients to read, search, and send WhatsApp messages through a personal account. It provides comprehensive tools for managing chats, groups, and contacts using an existing authenticated WhatsApp session.Last updated27
- Flicense-qualityDmaintenanceWhatsApp MCP server that exposes messaging, groups, contacts, and profile management as tools and resources for AI agents, supporting Baileys and Meta Cloud API.Last updated19
- Alicense-qualityAmaintenanceA self-hosted WhatsApp bridge that exposes a stdio MCP server with ~20 tools for reading conversations, sending messages, managing groups, contacts, and aliases, enabling AI agents to operate WhatsApp directly.Last updated2MIT
Related MCP Connectors
Security-first WordPress MCP server. 129 tools for Claude, ChatGPT, Gemini. Free on wp.org.
Managed LinkedIn MCP server for AI agents: search, connect, message and enrich on accounts you own.
Hosted MCP endpoint with realistic fake data for prototyping agents. 12 tools, no setup.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/HalemoGPA/whatsapp-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server