Safe WhatsApp MCP
A local MCP server that enables an agent to read personal WhatsApp messages, prepare replies, and send them only after user confirmation.
README
Safe WhatsApp MCP
A local Model Context Protocol (MCP) server that lets an agent read a personal WhatsApp linked device, prepare an exact reply, and send only after a separate confirmation-gated tool call.
It uses your personal WhatsApp linked-device session. It does not use Meta's WhatsApp Business Platform or Cloud API, and it does not require a business account.
[!WARNING] This project uses the unofficial Baileys WhatsApp Web protocol. It is not affiliated with or endorsed by WhatsApp or Meta, and use may violate WhatsApp's terms or cause an account restriction. Do not use it for spam, bulk messaging, scraping, or unattended automation.
Project note
Safe WhatsApp MCP was initially built for a personal Bliss AI workflow. Much of the implementation was AI-assisted, and it has not received an independent security audit. It was designed around a narrow, confirmation-gated workflow, but you should still review it before pairing a primary account or exposing sensitive conversations to an AI model.
Issues, security-minded reviews, and small auditable improvements are welcome. See CONTRIBUTING.md and report vulnerabilities privately as described in SECURITY.md.
Project status
Version 0.1.0 is an early public source preview. It is not published to npm and there are no signed GitHub release downloads yet. The supported public path today is to review the source and build a local standalone bundle for macOS or Linux.
Quick start
The current public install path builds a self-contained launcher from this repository. Building requires Node.js 22; using the resulting launcher does not.
git clone https://github.com/dhruvratra/safe-whatsapp-mcp.git
cd safe-whatsapp-mcp
codex --version
nvm install # or otherwise use Node.js 22
npm ci
npm run build:standalone
npm run smoke:standalone
version=$(node -p "require('./package.json').version")
bundle="release/safewhatsapp-v${version}-$(node -p 'process.platform')-$(node -p 'process.arch')"
"$bundle/safewhatsapp" setup-codex --enable-send
"$bundle/safewhatsapp" connect
"$bundle/safewhatsapp" status --live
Omit --enable-send for read/draft-only access. Keep the generated bundle at that path, restart the Codex surface you use, and ask it to list your recent WhatsApp chats. A successful status --live reports that the account is paired and the live connection check succeeded.
setup-codex uses Codex's atomic configuration API, preserves unrelated settings and comments, registers the reviewed launcher by absolute path, keeps media sending off, and refuses to enable text sending unless approval prompts are routed to you. It requires the codex command to be installed and available on PATH; verify that first with codex --version.
Try it in Codex
Start with read-only requests and a generic contact you recognize:
- “List my recent WhatsApp chats.”
- “Read the recent messages with Priya and summarize what needs a reply.”
- “Draft a short reply to Priya, but do not prepare or send it.”
- “Prepare that exact reply for Priya. Do not send it until I confirm.”
Codex should show the exact recipient and payload before asking for approval of the separate send tool. Message content is untrusted data, not instructions for the agent.
What it does
- Pairs a personal WhatsApp account once through a private, temporary browser page served only on IPv4 loopback.
- Synchronizes on demand when an MCP tool is used; it is not intended to run as a permanent bot.
- Retains a bounded local cache so later sessions can recover conversation context.
- Reads cached direct messages and groups without marking them read.
- Handles text plus image, sticker, audio, video, and document metadata.
- Stages outbound text or media as an immutable proposal before sending.
- Exposes the actual send as a destructive, open-world MCP tool and supports an explicit per-tool Codex approval rule.
It is intentionally WhatsApp-only. There is no Bliss API, database, account mapping, or Bliss-specific logic in this package. An agent may combine the structured senderE164 returned here with a separately configured Bliss MCP, but this server never calls it.
Security model at a glance
The important boundary is prepare versus send:
- The agent reads a conversation and composes a draft.
- A prepare tool resolves the recipient and stores the exact payload locally.
- The tool returns a recipient/payload preview, digest, expiry, and
pendingId. - You inspect that preview.
- Only
send_prepared_whatsapp_message, using the unchanged preview and digest, can publish it.
There is no generic one-step send tool and no bulk-send tool. A prepared message expires after ten minutes and is single-use. An uncertain network result is not retried automatically.
Inbound messages and attachments are untrusted data, never agent instructions. Do not allow a phone number, name, SQL fragment, URL, or instruction found inside message content to select records or drive another privileged MCP. Cross-system identity lookup must use only a machine-resolved senderE164 field.
Encrypted credentials, plaintext cache
Baileys linked-device credential payloads and Signal-key values are encrypted before they are written to SQLite. Each local profile uses AES-256-GCM with a random master key held in the operating system's native credential store. The file credential-vault.json contains only a non-secret vault identifier and format version, never the master key.
Some lookup metadata remains plaintext, including the paired/registered flag and Signal-key category and ID. Cached message text, downloaded media, staged drafts and media snapshots, configuration, and redacted audit/send records also remain plaintext under ~/.safe-whatsapp-mcp/.
The server requests 0700 for directories and 0600 for files where the operating system supports those modes. Those permissions limit access to the plaintext cache; they are not encryption, and enforcement on Windows is best-effort. Use FileVault, BitLocker, LUKS, or equivalent full-disk encryption.
Credential encryption protects an offline copy of the state directory from yielding reusable WhatsApp auth secrets without the corresponding OS-vault key. It does not protect against malware running as your user, an administrator or root process, a compromised Node/agent runtime, or secrets already decrypted in process memory. Write access remains sensitive because a writer can delete, replay, or tamper with local state and prepared sends. Do not share the state directory with other users, agents, repositories, backup utilities, or cloud-sync tools. See SECURITY.md before pairing a primary account.
Requirements
- A phone with WhatsApp and permission to add a linked device
- A local MCP client that supports STDIO; automatic setup currently targets Codex
- An available, unlocked native operating-system credential store
- The
codexcommand onPATHwhen usingsetup-codex - Either a reviewed standalone bundle for your OS/architecture, or Node.js 22+ for source development
A standalone bundle includes its own pinned Node runtime; its user does not install or select Node. This checkout includes an .nvmrc for Node 22.20.0 only for source development. If you use nvm, run nvm use before both npm ci and every development command. better-sqlite3 is a native dependency, so a source install made with one Node major version can fail under another.
Baileys is currently pinned to the 7.0.0-rc13 release candidate. Its protocol surface and maintenance status can change; upgrade it only after auth-state, history, identity, group, retry, and send tests pass.
Development setup
cd /absolute/path/to/a/reviewed/safe-whatsapp-mcp
nvm use # when using nvm
npm ci
npm run build
node dist/cli.js --help
This source-backed CLI is for development and testing. setup-codex intentionally refuses to register a launcher from a source checkout because its runtime can move or change underneath Codex. Build the standalone bundle below for automatic Codex registration. Do not use npx safe-whatsapp-mcp until an official package is published from this repository.
Build a standalone bundle
On the build machine, run:
npm run build:standalone
npm run smoke:standalone
This creates release/safewhatsapp-v<version>-<os>-<arch>/ plus a compressed archive on macOS and Linux. Keep the directory intact: its safewhatsapp launcher always invokes the private runtime beside it and never resolves node from PATH. The smoke check extracts the produced archive into a clean temporary directory, removes Node from PATH, and exercises the local SQLite-backed status command through both the direct and symlinked launcher.
To install a reviewed archive without Node or npm, extract its whole directory to a stable location and link only its launcher onto PATH. For example, set bundle_name to the archive name for your version and target:
bundle_name=safewhatsapp-v0.1.0-darwin-arm64
mkdir -p "$HOME/.local/lib/safewhatsapp" "$HOME/.local/bin"
tar -xzf "release/$bundle_name.tar.gz" -C "$HOME/.local/lib/safewhatsapp"
ln -sfn "$HOME/.local/lib/safewhatsapp/$bundle_name/safewhatsapp" "$HOME/.local/bin/safewhatsapp"
Ensure $HOME/.local/bin is on the MCP client's PATH. Do not copy the launcher by itself; it needs the adjacent runtime/ and app/ directories. A signed installer can perform these steps for public releases.
Release bundles are platform- and architecture-specific because better-sqlite3 and the operating-system credential-store adapter are native modules. Build each supported macOS/Linux target on that target with the same Node executable used to install its production dependencies. Windows standalone packaging is not implemented yet. A publicly downloaded macOS release must still be signed and notarized; the local builder does not claim to do that without the maintainer's Apple credentials.
Pair the linked device
safewhatsapp connect
The command opens a crisp QR in your default browser. Scan it from WhatsApp → Settings → Linked Devices → Link a device. If the browser cannot be opened automatically, the terminal prints a short-lived local URL to open yourself.
The QR page binds only to 127.0.0.1 on an operating-system-selected port and uses a random 256-bit path token. The QR payload and PNG are not written to app state, temporary files, logs, audit data, MCP, or a remote service. They necessarily exist transiently in the Baileys/Node process, loopback HTTP response, and browser memory; owned PNG buffers are cleared on replacement/exit as a best effort. Responses are marked no-store. The document stays loaded while a token-protected same-origin poll updates only the QR image when WhatsApp rotates it. After the scan, the page removes the QR and shows the finishing state until the required credential save and WhatsApp socket restart succeed; it reports success only after the replacement socket opens. If the local process stops, the loaded page tells you to check the terminal instead of navigating to a browser error. Browser history may retain only the now-useless loopback URL.
When the command starts with an unpaired profile, it first clears any residual credentials, messages, downloaded media, pending sends, and audit data from a prior account. This happens under the state lock before fresh credentials are loaded; configuration and user-owned outbox/ files are preserved. QR pairing persists the encrypted credentials before Baileys performs WhatsApp's mandatory post-scan reconnect, then waits up to two minutes for the final recent-history chunk to be ingested before disconnecting. Pending offline notifications and the first history chunk are not treated as completion. Full-history registration is deliberately disabled because the local cache is bounded and that mode is not accepted reliably by WhatsApp's current linked-device handshake. Later reads reconnect with the saved credentials. Normal shutdown never logs out or unpairs the device.
Useful local commands:
safewhatsapp status
safewhatsapp status --live
safewhatsapp setup-codex
safewhatsapp setup-codex --enable-send
safewhatsapp serve
safewhatsapp disconnect
safewhatsapp purge --yes
safewhatsapp purge --yes --abandon-key # recovery only; see below
statusreads local state only;status --liveperforms a bounded connection check. Status reports credential encryption and plaintext cache storage separately ascredentialsAtRestandmessageCacheAtRest.setup-codexregisters read/draft access in the user Codex configuration.--enable-sendexplicitly enables text sends while retaining the mandatory per-tool human approval; media sending remains disabled.disconnectimmediately sends a remote logout request when a paired local credential exists, then clears all local account-bound state and requests deletion of that profile's OS credential-vault key while preserving configuration andoutbox/. Baileys does not provide a server acknowledgement for that request. If the remote request fails after credentials are loaded, local cleanup still completes and the command reports the failure.unlinkremains an undocumented compatibility alias.purge --yesis local-only. It removes the encrypted credentials, requests deletion of their OS credential-vault key, and clears cache, downloaded inbound media, pending sends, audit data, and configuration while preservingoutbox/; it does not log out WhatsApp. Rundisconnectfirst when possible, or remove the device in WhatsApp's Linked Devices screen.- If key deletion cannot be confirmed, normal purge retains the non-secret vault descriptor so cleanup can be retried. After the encrypted auth rows have been removed from the active state directory,
purge --yes --abandon-keyis the explicit recovery path: it discards that descriptor and permits re-pairing even if an orphaned wrapping key may remain in the OS credential store.
Credential-vault deletion is best effort because the native binding can suppress some operating-system errors. Use --abandon-key only after credential_cleanup_incomplete and only after removing retained copies of the matching state. Destructive cleanup also requires this package's private ownership marker, so it fails closed for an arbitrary directory. See SECURITY.md for the complete cleanup and recovery model.
Optional local settings
Defaults can be lowered or raised within hard safety ceilings in ~/.safe-whatsapp-mcp/config.json; start from examples/config.example.json. Every value must be positive, maxMessagesPerChat must be an integer, and inlineMediaMiB cannot exceed maxMediaMiB.
| Setting | Default | Maximum |
|---|---|---|
retentionDays |
7 | 3,650 |
maxMessagesPerChat |
200 | 10,000 |
pendingTtlMinutes |
10 | 1,440 |
connectionTimeoutSeconds |
15 | 600 |
syncTimeoutSeconds |
15 | 120 |
idleTimeoutSeconds |
60 | 3,600 |
inlineMediaMiB |
8 | 8 |
maxMediaMiB |
25 | 25 |
connectionTimeoutSeconds applies to ordinary linked-device reconnects. Interactive QR pairing uses a separate five-minute connection window and a two-minute recent-history window.
Tests or isolated local profiles may set SAFE_WHATSAPP_MCP_STATE_DIR to an absolute state-directory path. Do not point it at a repository, shared folder, cloud-synchronized directory, or a directory containing unrelated files.
Configure Codex
Use the reviewed standalone installation to configure Codex automatically:
safewhatsapp setup-codex # read and draft; sending off
safewhatsapp setup-codex --enable-send # confirmation-gated text sending
This updates only mcp_servers.safe_whatsapp through Codex's atomic configuration API. It does not change the user's global model, sandbox, approval policy, or approval reviewer. A same-name server with a different command is treated as a conflict instead of being overwritten. Existing stricter server approval settings, disabled state, and tool deny list are preserved. Run the command again after installing a newer standalone bundle so Codex follows the new verified launcher.
If an existing Safe WhatsApp entry is disabled, setup leaves it disabled and says so. Enable that entry in Codex before restarting if you want its tools loaded.
--enable-send fails if effective Codex settings use approval_policy = "never" or route approvals to an automated reviewer. Project, profile, session, or managed configuration can still override the user configuration; review those layers if Codex reports the server as overridden.
Restart the Codex surface you use after setup. The ChatGPT desktop app, Codex CLI, and IDE extension on the same host share the MCP configuration.
For manual review or another machine, examples/codex-config.toml shows the generated server policy. Replace its launcher placeholder with the absolute path to a reviewed standalone safewhatsapp; never configure Codex against node, npx, a checkout's dist/cli.js, or a launcher copied without its adjacent bundle.
The example follows the current official Codex MCP configuration, including server-level and per-tool approval modes.
The important approval settings are shown below. The first two are global Codex settings, so review their effect on your other tools before changing them; setup-codex checks them but never changes them.
approval_policy = "on-request"
approvals_reviewer = "user"
[mcp_servers.safe_whatsapp]
command = "/absolute/path/to/reviewed/standalone/safewhatsapp"
args = ["serve"]
default_tools_approval_mode = "writes"
[mcp_servers.safe_whatsapp.env]
SAFE_WHATSAPP_MCP_ENABLE_SEND = "false"
SAFE_WHATSAPP_MCP_ENABLE_MEDIA_SEND = "false"
[mcp_servers.safe_whatsapp.tools.send_prepared_whatsapp_message]
approval_mode = "prompt"
approval_policy = "on-request" allows interactive MCP prompts, and approvals_reviewer = "user" prevents them from being delegated to an automatic reviewer. default_tools_approval_mode = "writes" prompts for tools that are not marked read-only, while the explicit per-tool rule forces a human prompt for the external send. The server also marks that tool destructive and open-world. Set the text-send environment gate to true only after these settings are effective. Do not weaken them for routine use.
For another STDIO client, start from examples/stdio-client.example.json and configure that client's equivalent of “always prompt before this tool.” If the client cannot enforce per-tool approval, leave sending disabled.
Sending controls
Sending is off unless explicitly enabled in the MCP server environment:
| Variable | Default | Effect |
|---|---|---|
SAFE_WHATSAPP_MCP_ENABLE_SEND |
false |
Allows prepared text sends and is also required for media sends. |
SAFE_WHATSAPP_MCP_ENABLE_MEDIA_SEND |
false |
Additionally allows prepared media sends. |
These flags are a deployment gate, not user confirmation. Every message still follows prepare → inspect → approved send.
Direct destinations must be canonical +E.164 numbers and are verified with WhatsApp. Groups must already exist and are addressed through their opaque chat IDs. There is deliberately no send allowlist in 0.1.0: after staging and approval, a send can target any verified direct number or existing group. Broadcasts, channels, status, and arbitrary raw JIDs are rejected.
The approval preview visibly escapes bidirectional and other invisible Unicode formatting controls. This makes recipient names and message text inspectable without changing the bytes that will actually be sent; the digest binds both the exact payload and that displayed preview.
For outbound media, first place the file in:
~/.safe-whatsapp-mcp/outbox/
The media prepare tool accepts only a relative path beneath that directory. Absolute paths, traversal, symlink escapes, non-regular files, and files over the configured limit (hard maximum 25 MiB) are rejected. Preparation snapshots the exact bytes into private pending storage and binds their hash and metadata into the digest. The server cannot send an arbitrary workspace or home-directory file. Images, video, audio, and documents are supported; audio captions are rejected because WhatsApp's audio payload does not carry them.
MCP tools
| Tool | Purpose | Remote side effect |
|---|---|---|
get_whatsapp_status |
Pairing, cache, sync, retention, and feature status | No |
list_whatsapp_chats |
Paginated direct/group summaries | No |
read_whatsapp_chat |
Paginated retained messages without marking read | No |
fetch_older_whatsapp_messages |
Request one best-effort batch of older messages for a cached chat | No chat mutation; writes retained local cache |
search_whatsapp_messages |
Search retained text and captions | No |
get_whatsapp_media |
Explicitly decrypt one retained attachment | No |
list_whatsapp_sends |
Inspect staged and historical send records | No |
prepare_whatsapp_text_send |
Stage an exact text payload | No; writes local state |
prepare_whatsapp_media_send |
Snapshot and stage outbox media | No; writes local state |
send_prepared_whatsapp_message |
Send one immutable staged payload | Yes; always approve |
discard_prepared_whatsapp_message |
Remove one unsent staged payload | No; writes local state |
All cached direct and group conversations are readable. 0.1.0 has no read allowlist. Keep this in mind before giving an agent other privileged tools in the same conversation.
fetch_older_whatsapp_messages makes one bounded request for at most 50 messages. It never follows the history automatically; each additional batch requires another explicit tool call. Current Baileys companion-device behavior is best effort: WhatsApp may accept a request without delivering the history response before the bounded wait ends. In that case the tool reports pending rather than claiming that the chat has no older history.
When a batch arrives, newlyRetainedCount and anchorAdvanced distinguish real paging progress from a duplicate response. An optional beforeMessageId is accepted only when it still identifies the current oldest retained message, preventing a stale cursor from replacing older cache entries near the per-chat limit.
Fetched messages pass through the same deletion, expiry, view-once, and deduplication rules as synchronized messages. They also obey the configured retentionDays and maxMessagesPerChat limits, so a batch outside those bounds may not remain in the local cache. Raise those settings deliberately before retaining more history; the expanded cache remains plaintext under private file permissions.
Data behavior
- Default retention is seven days and at most 200 messages per chat.
- Synchronization may report
partial; a linked device cannot make a stateless, complete inbox fetch on every launch. - Older-history fetching is one best-effort batch of at most 50 messages per explicit call; it does not page automatically, and
pendingdoes not mean the end of history. - Incoming history and live updates are deduplicated. Edits, revocations, deletions, and disappearing-message expiry are applied before data is exposed.
- Direct-chat edits, revocations, clear events, and tombstones propagate across WhatsApp's verified PN/LID aliases, including when that alias mapping arrives after the deletion.
- View-once media is never persisted or exposed.
- Attachment bytes are downloaded only after
get_whatsapp_mediais called, up to 25 MiB. - Retained media stores only a bounded WhatsApp
directPathand media key. Message-supplied download URLs are discarded; the production downloader constructs the request against Baileys' fixed WhatsApp media host. - Downloaded bytes are signature-sniffed. Only matching, recognized raster-image or audio bytes up to 8 MiB may be returned inline; SVG, mismatched content, video, documents, and larger content are exposed as opaque MCP resources.
- A media message is reauthorized after download/cache awaits and again when an opaque resource is read, so a concurrent revocation or expiry invalidates its cache instead of returning stale bytes.
- Downloaded attachment bytes are reconciled to retained message IDs, so expiry, deletion, and the seven-day/200-message limits also remove their cache.
- A new pairing cannot inherit an old account's cache or staged sends: an unpaired profile is cleared before fresh in-memory auth state is created.
- The socket closes after 60 seconds of inactivity and reconnects on demand.
- The server does not send presence, typing, or read receipts, and does not archive, mute, or otherwise mutate chats.
- Persistent database-backed WhatsApp message retry lookup is disabled, preventing reconnect-time relay of unrelated
fromMemessages outside this package's staged-send path. - Group sends do not reuse persisted participant metadata; Baileys fetches current group metadata before constructing a send.
- Group roster metadata is not retained at all; group events keep only the bounded chat title needed by the read/approval UI.
- Redacted send/audit metadata is retained for 30 days. Logs must not include message text, QR data, full phone/JID values, contact/group names, filenames, or auth material.
WhatsApp content is end-to-end encrypted in transit to the linked-device endpoint. When an MCP client sends that plaintext to a configured AI model, it has left the WhatsApp encryption boundary. Minimize the messages and media you provide to any model and understand that model provider's data controls.
Scope exclusions
0.1.0 does not support group administration, broadcasts, channels, status, reactions, outbound edit/delete, calls, location, contacts, polls, view-once sending, auto-replies, scheduled sends, or bulk sends.
Development
npm run typecheck
npm test
npm run audit:prod
npm run pack:dry-run
npm run smoke:pack
Tests use injected fake sockets; they must not connect to WhatsApp. A live personal-account acceptance test is manual, opt-in, and runs only after automated security and packaging checks pass. See CONTRIBUTING.md.
Status and license
The source repository is public, but the package has not been published to npm and there is no signed GitHub release yet. Recheck the package name immediately before a separate, explicitly approved publish operation.
Licensed under the MIT License. Use GitHub Issues for non-sensitive bugs and feature requests, CONTRIBUTING.md for development guidance, and SECURITY.md for private vulnerability reporting.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。