wacli-mcp
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.
wacli-mcp
A Model Context Protocol server that lets an AI assistant search and read a WhatsApp archive mirrored locally by wacli, and, only if you opt in, send plain text to a short list of recipients you name.
By default it is read-only. It never edits, deletes, reacts, marks as read, downloads media, back-fills history,
or links an account; those code paths do not exist here. Keeping the archive current, and owning the WhatsApp
connection, is left to wacli sync --follow, running as a separate process you control.
The design follows the MCP guidance in wacli's own companion integrations guide: wrap the JSON CLI, fix the store, allowlist a few read commands, pass arguments without a shell, bound everything, and treat retrieved content as untrusted. docs/assessment.md explains why this wraps wacli instead of using whatsmeow directly, and what was evaluated.
wacli and whatsmeow use WhatsApp's unofficial multi-device web protocol. They are not affiliated with WhatsApp or Meta, and WhatsApp's help center says linking an account to unofficial apps may result in a temporary or permanent ban. This project never links an account itself, but it is only useful with one that wacli has linked.
Tools
Tool | wacli command (always prefixed with |
|
|
|
|
|
|
|
|
|
|
|
|
Every tool carries readOnlyHint: true and openWorldHint: false. Results are wrapped with
untrusted_content: true and a notice telling the model not to follow instructions found in messages.
With sending enabled (see Sending) one more tool appears:
Tool | wacli command |
|
|
It carries readOnlyHint: false, idempotentHint: false and openWorldHint: true.
With several accounts configured (see Multiple accounts), every tool takes a required
account argument limited to the configured aliases. With one account, tool signatures are unchanged.
Related MCP server: WhatsApp Read Agent MCP Server
Security model
Read-only access still exposes private conversations, and a model that reads attacker-controlled text can be steered by it (Invariant Labs showed exactly this against a WhatsApp MCP server). The layers, from outside in:
No write tools by default. The exfiltration path in the Invariant Labs research was a
send_messagetool. It does not exist unless you enable it, and then only reaches recipients you listed (see Sending). Pair this server with other tools that can send data out (email, web requests, shell) only with care, since a prompt injection read through this server could use them.No network for wacli. By default every wacli call runs inside an empty network namespace (
unshare -rn), so even a bug in a read path cannot talk to WhatsApp. The server refuses to start if that is unavailable, unless you setWACLI_MCP_NETWORK_ISOLATION=autooroff.wacli read-only mode. Every read call passes
--read-onlyandWACLI_READONLY=1. The one exception issend_message, which still runs without network and can only hand a message to the sync daemon.Fixed command shape. The executable, store or account, and command allowlist are set by the operator. Arguments are single
--flag=valuetokens, free text goes after--, there is no shell, and the child gets a minimal environment (your API keys and tokens are not inherited).Bounds. Per-call timeout with process-group kill, stdout cap, concurrency cap, a per-minute rate limit, result limits, and per-field text truncation.
Chat policy. Optional allowlist or denylist of chat JIDs, enforced on explicit filters and again on every result row.
Output shaping. Only known fields are returned. Local media paths and WhatsApp CDN paths are dropped. Bidirectional-override, zero-width and control characters are stripped so text cannot disguise itself.
Executable vetting. The wacli binary and its directory must not be writable by group or others; you can also pin its SHA-256.
Audit log. Optional JSON-lines log (mode 0600) with tool name, outcome, duration and result counts. It never records arguments, because queries and names are personal data.
HTTP transport (optional). Bearer token from a mode-0600 file, constant-time comparison, binding limited to loopback or Tailscale addresses, Host header allowlist (DNS rebinding protection), and browser
Originrequests refused.
Things this server cannot protect against: an MCP client that shows results to someone else, other tools in the same agent session, and anyone who can read the wacli store directly (it is plain SQLite, protected only by file permissions).
Requirements
Linux with unprivileged user namespaces (for network isolation). Debian 13 works out of the box; Ubuntu 24.04 needs
kernel.apparmor_restrict_unprivileged_userns=0.Python 3.12+ and uv.
wacli 0.22 or newer (tested with 0.25.1), already authenticated by you with
wacli auth, and ideally kept fresh withwacli sync --follow.
Install
git clone https://github.com/viniciusbregoli/wacli-mcp.git
cd wacli-mcp
uv tool install .
wacli-mcp check # validates the configuration and runs a local `wacli doctor`Configuration
All settings are environment variables read once at startup. None can be changed by the model.
Variable | Default | Meaning |
|
| Absolute path to wacli |
| unset | Refuse to start unless the binary matches |
| wacli's default | Store directory (exclusive with |
| unset | Named wacli account |
|
|
|
| unset | Comma-separated JIDs; when set, only these chats are visible |
| unset | Comma-separated JIDs that are never visible |
|
| Per-call timeout (1 to 120) |
|
| Cap on wacli stdout per call |
|
| Parallel wacli processes |
|
| Tool calls per minute |
|
| Truncation length for each text field |
| unset | Absolute path of the JSON-lines audit log |
|
|
|
| unset | TOML file defining several accounts (see Multiple accounts) |
Sending only (see Sending):
Variable | Default | Meaning |
| unset |
|
| unset | Comma-separated |
| Required with sending, so the daemon check looks at the right store | |
|
|
|
|
| Longest message accepted (1 to 4096) |
|
| Sends per rolling hour (1 to 60) |
|
| Time allowed for one send (10 to 300) |
HTTP transport only:
Variable | Default | Meaning |
|
| Loopback or a Tailscale address; anything else is refused |
|
| Port |
| required | Mode-0600 file holding a bearer token of at least 32 characters |
| unset | Extra |
Policy notes: with an allowlist, explicit chat filters must be exact JIDs, because names are resolved inside
wacli. wacli may return rows for a chat's @lid identity when you ask for its phone-number JID, so list both forms
if you use an allowlist. Rows are filtered after wacli applies limit, so a page can come back shorter than
requested; hidden_by_policy reports how many rows were removed.
Multiple accounts
One server can serve several WhatsApp accounts, each a separate wacli store. Point WACLI_MCP_ACCOUNTS_FILE at a
TOML file (owned by you, mode 600) with one table per account; deploy/accounts.example.toml
is a starting point:
[accounts.personal]
store = "/home/me/.local/state/wacli" # an existing store, by absolute path
label = "Personal phone"
denied_chats = ["15550000002@s.whatsapp.net"]
[accounts.personal.send] # optional, needs WACLI_MCP_ENABLE_SEND=1
recipients = { me = "15550000001@s.whatsapp.net" }
[accounts.work]
wacli_account = "work" # a named wacli account
allowed_chats = ["120363000000000001@g.us"]Key | Meaning |
| Absolute store directory. Exactly one of |
| A name from |
| Optional text shown to the model next to the alias |
| This account's chat policy (lists of JIDs) |
| This account's only possible recipients, |
How accounts are kept apart:
Explicit selection. Every tool requires
account, an enum of the aliases. There is no default account, so a call can never land on an archive the model did not name.Pinned stores.
wacli_accountnames are resolved once at startup withwacli accounts show(read-only, inside the empty network namespace) and every call then uses--store=<that directory>. Changing wacli'sconfig.yamlor default account later cannot redirect an alias; restart the server to pick up changes. Two aliases on the same store are refused.Per-account policy. Chat allowlists and denylists, send recipients, the hourly send limit, the duplicate guard and the sync daemon check all belong to one account. A recipient alias or JID configured for one account is refused when sending from another.
Two switches for sending. The file says who each account may message, and
WACLI_MCP_ENABLE_SEND=1in the environment turns sending on.send_messageonly lists accounts that have recipients.Labelled results and audit. Results and audit entries carry the account alias.
WACLI_MCP_STORE, WACLI_MCP_ACCOUNT, WACLI_MCP_ALLOWED_CHATS, WACLI_MCP_DENIED_CHATS and
WACLI_MCP_SEND_RECIPIENTS cannot be combined with the file; everything else (limits, audit log, isolation, send
confirmation) still comes from the environment and applies to all accounts. The global per-minute rate limit and
concurrency cap are shared, while send limits are per account. Each account needs its own
wacli --store=<dir> sync --follow running for fresh data and for sending.
Adding a second wacli account without breaking the first: wacli accounts add <name> makes the first account it
adds the default, so every wacli command run without --store or --account (including a single-account
wacli-mcp without WACLI_MCP_STORE) switches to the new, empty store. Pin the existing archive by absolute path,
as in the example above, before adding accounts; wacli-mcp check warns when a single-account setup is unpinned.
Connecting a client
Local clients should spawn the server over stdio. For Claude Code:
claude mcp add whatsapp-archive --scope user \
--env WACLI_MCP_AUDIT_LOG="$HOME/.local/state/wacli-mcp/audit.jsonl" \
-- wacli-mcp stdioAny client that accepts a JSON server definition:
{
"mcpServers": {
"whatsapp-archive": {
"command": "wacli-mcp",
"args": ["stdio"],
"env": { "WACLI_MCP_DENIED_CHATS": "15551234567@s.whatsapp.net" }
}
}
}Remote clients over Tailscale
install -d -m 700 ~/.config/wacli-mcp ~/.local/state/wacli-mcp
(umask 077; python3 -c 'import secrets; print(secrets.token_urlsafe(32))' > ~/.config/wacli-mcp/token)
WACLI_MCP_HTTP_HOST="$(tailscale ip -4)" WACLI_MCP_TOKEN_FILE=~/.config/wacli-mcp/token wacli-mcp httpClients connect to http://<tailscale-ip>:8765/mcp with Authorization: Bearer <token>. Traffic is plain HTTP
inside the Tailscale tunnel, which WireGuard already encrypts. A hardened systemd unit is in
deploy/wacli-mcp-http.service.
Sending
Sending is off unless you set WACLI_MCP_ENABLE_SEND=1. It exists for one job: letting an assistant send a short
text you asked for to someone you named in advance. It is the one capability that can move data out of the
archive, so a message passes all of these, in order, before wacli is run:
Recipient allowlist.
recipientmust be an alias fromWACLI_MCP_SEND_RECIPIENTSor one of its exact JIDs. Names, phone numbers and other JIDs are refused, so a model (or a message it read) cannot pick a new destination. Recipients must also be visible under the chat policy, or the server will not start.Message checks. Plain text up to
WACLI_MCP_SEND_MAX_CHARS. Text with control or invisible characters (bidi overrides, zero-width spaces) is refused rather than cleaned, so what you approve is exactly what is sent. Lines starting with-----are refused because the confirmation prompt uses that frame.Rate and duplicates. At most
WACLI_MCP_SEND_PER_HOURsends per hour, one at a time, and the same text to the same recipient is refused for 10 minutes.Daemon check. The server confirms that
wacli sync --followholds the store lock and its.send.sockexists. wacli then hands the message to that daemon over the Unix socket; the wacli child still runs in the empty network namespace. Without the daemon nothing is run, so this server never opens a second WhatsApp connection.User confirmation. With the default
WACLI_MCP_SEND_CONFIRMATION=elicit, the client shows the user the recipient and the exact text (MCP elicitation) and nothing is sent unless they approve. Clients without elicitation support get an error and nothing is sent.
Outcomes are reported honestly. If wacli times out or says the message may still have gone through, the tool says the outcome is unknown, tells the model not to retry, and blocks the same text for 10 minutes. The audit log records the recipient alias, character count and outcome of every attempt, including refused ones, and never the text.
WACLI_MCP_SEND_CONFIRMATION=none removes step 5. Use it only for a client without elicitation, with a recipient
list you would be comfortable with a prompt injection writing to (for example, only yourself): anything the model
reads can then influence what it sends to those recipients.
Example, sending only to your own number and one group:
claude mcp add whatsapp --scope user \
--env WACLI_MCP_STORE="$HOME/.local/state/wacli" \
--env WACLI_MCP_ENABLE_SEND=1 \
--env WACLI_MCP_SEND_RECIPIENTS="me=15551234567@s.whatsapp.net,family=120363000000000000@g.us" \
--env WACLI_MCP_AUDIT_LOG="$HOME/.local/state/wacli-mcp/audit.jsonl" \
-- wacli-mcp stdioarchive_health reports send_ready: true once the daemon is running for that store.
Keeping the archive fresh
This server never syncs. Results describe what wacli has stored; message results (list_messages,
search_messages) and archive_health include last_sync_at, which says when a sync last completed. Run the
sync yourself, for example as a service (sending also needs it running):
wacli sync --followwacli's read commands work while sync --follow holds the store lock.
Limitations
WhatsApp gives linked devices history on a best-effort basis; see
history_coverage.Message content is returned to the model, which is the point of the tool. Use the chat policy for chats that should never leave the machine.
wacli's JSON fields are not a formal API. Integration tests pin the behaviour of the tested version; run them after upgrading wacli.
Media is not available: there is no download tool, by design. Sending is plain text only: no files, replies, mentions or link previews.
Delivery is not confirmed. A successful
send_messagemeans WhatsApp accepted the message, not that the recipient's phone received it.
Development
uv sync
uv run ruff check . && uv run ruff format --check .
uv run pytestUnit tests use a fake wacli executable. Integration tests (-m integration) run the real wacli binary
(WACLI_MCP_TEST_BIN, default ~/.local/bin/wacli) against a throwaway store created in a temp directory with
synthetic data, inside an empty network namespace. They never open your real store. CI downloads the pinned
official wacli release and verifies its checksum before running them.
License
MIT. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Read, search and reply to your WhatsApp chats from your AI, using your own number.
Read-only WhatsApp MCP for your AI agent: it reads the chats you choose. Try the public demo.
Send WhatsApp messages from your own number through your AI. It cannot read your chats.
1Your own WhatsApp in Claude and ChatGPT: read chats, draft replies, send messages you approve.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to interact with your personal WhatsApp account, allowing them to search messages and contacts, retrieve chat history, and send messages to individuals or groups. Uses WhatsApp Web API with local data storage for privacy and security.16 npmISC
- FlicenseAqualityCmaintenanceEnables read-only access to your own WhatsApp history and attachments via the Model Context Protocol, allowing AI assistants to enumerate chats, page history deterministically, and retrieve approved attachment bytes.7-
- AlicenseNot gradedqualityCmaintenanceEnables AI assistants to read WhatsApp chat history from a SQLite archive, optionally scrape live chats, and send messages through a chain of safety guards.35 npmMIT
- AlicenseBqualityBmaintenanceEnables AI clients to read and send WhatsApp chats, messages, contacts, groups, polls, reactions and media through two isolated adapters — a personal linked-device runtime and the official Meta Business Graph API — each with per-profile credentials, audit/history storage and managed media. Writes are gated behind prepare/confirm workflows, and inbound media or history is restricted to verified provider IDs and bounded, allowlisted downloads.1MIT