Skip to content
HN On Hacker News ↗

GitHub - finereli/whatsapp-mcp: A read-only MCP server that lets your AI agent search and read your own WhatsApp messages. Runs on Cloudflare Workers.

▲ 157 points • 85 comments • by zoozla • 3w ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is AI.

99 %

AI likelihood · overall

AI
0% human-written 100% AI-generated
SEGMENTS · HUMAN 0 of 1
SEGMENTS · AI 1 of 1
WORD COUNT 1,615
PEAK AI % 99% · §1
Analyzed
Oct 18
backend: pangram/v3.3
Segments scanned
1 windows
avg 1615 words each
Distribution
0 / 100%
human / AI fraction
Verdict
AI
Pangram v3.3

Article text · 1,615 words · 1 segments analyzed

Human AI-generated
§1 AI · 99%

Let your AI agent read your WhatsApp messages. This is a small server that runs in your own Cloudflare account. You link your phone to it once, the way you'd link WhatsApp Web, and from then on any MCP client (Claude Code, Claude Desktop, anything that speaks MCP) can search and read your chats. It can read and search your messages, and remember names you give it for phone numbers. It can't send anything. Ask your agent things like: "What did Dana say about the flights?" "Find every message where someone mentioned the lease, across all my chats." "Summarize what I missed in the family group this week." How it works Your phone stays the source of truth. The server is a linked device that your agent talks to. It doesn't stay connected to WhatsApp. When your agent asks for something, and once a day, it connects, collects whatever WhatsApp queued for it since last time, writes that to its own database, and hangs up. Your phone keeps working normally and keeps getting notifications. Your messages live in a SQLite database inside a Durable Object in your Cloudflare account. Nothing goes anywhere else. Set it up You need a Cloudflare account on the Workers Paid plan (see the note below), Node 20 or later, and your phone. git clone https://github.com/finereli/whatsapp-mcp cd whatsapp-mcp npm install # 1. A place for the sign-in tokens. Paste the id it prints into wrangler.jsonc. npx wrangler kv namespace create OAUTH_KV # 2. A password for the page that links your phone. 12+ characters. npx wrangler secret put OWNER_PASSWORD # 3. Deploy. npm run deploy Wrangler prints your URL, something like https://whatsapp-mcp.your-name.workers.dev. Open it. The front page has the two remaining steps: Link WhatsApp. Scan the QR code with WhatsApp (Settings, Linked Devices, Link a Device). If you're on the phone itself and can't scan a screen, use the phone number option instead. The first link imports your history. The page shows progress, and you can close it. Connect your agent. In Claude Code: claude mcp add --transport http whatsapp https://your-url/mcp Then type /mcp and sign in with the password. Other clients: point them at the same /mcp URL. If you want your own domain, see the comments at the bottom of wrangler.jsonc. The Workers Paid note Importing a large history decodes a lot of data in one go, and the default CPU limit on Workers is too small for it. wrangler.jsonc raises it, and raising it needs the paid plan, which is a few dollars a month. I haven't tried the free plan. If you're on it, remove the limits block and expect the first import to be fragile. Things to know This isn't official. WhatsApp has no API for reading your own personal messages. This uses the same protocol WhatsApp Web uses, through an open source library (Baileys). That's against WhatsApp's terms. For a linked device that only reads, the practical risk is low, but it isn't zero. Use it on your own account, and don't use it to send messages. You get what WhatsApp sends. The first link imports whatever history WhatsApp decides to hand a new device. It may not be everything. It only does this once per link. To import again, relink (your stored messages are kept, and a re-import fills in anything missing). Some chats show up as phone numbers. WhatsApp doesn't give a linked device every contact name, and most of these are one-off messages from people you never saved. For the ones that matter, your agent can fix it: it lists the unnamed chats, looks the numbers up in your contacts with whatever contacts tool it already has (a Google Contacts or Gmail connector, for example), and saves the names. The server never connects to a contacts service itself. See "Naming chats" below. Media isn't downloaded. Photos, voice notes and documents appear as labels like [image] or [voice note 12s], with the caption if there was one. Nothing is transcribed. It's for one person. One deployment is one WhatsApp account and one owner. That's deliberate: each deployment holds a full message history and a live login to the account, so everyone should run their own. Relinking. If your phone says the device was removed, or you want to start over, ask your agent to run relink, or open /setup and choose "Unlink and scan again". You scan a new QR code. Stored messages are kept. What your agent can do Tool What it does list_chats Chats, most recent first. Filter by name or number. get_messages Read one chat. Page back or forward by date. search_messages Full-text search across everything, or within one chat. unnamed_chats Chats known only by phone number, busiest first, with the last thing the other person said. set_contact_names Save names for phone numbers, in any format. Names stick to the number, so they also show up in groups. sync_now Pull new messages right now. link_status Is it linked, when did it last pull, how much is stored. relink Unlink and start over. A human has to scan the new QR code. Every read tool first tops up from WhatsApp if the stored copy is more than ten minutes old. Naming chats WhatsApp only gives this server the names it knows. The rest show up as phone numbers. Rather than have the server log in to a contacts service (which means a cloud project, consent screens and, for Google, an app review), it hands the job to your agent, which usually already has a way to reach your contacts. Say "find names for my unnamed chats". The agent calls unnamed_chats, matches the numbers against your contacts, and saves what it finds with set_contact_names. It won't find everyone: most unnamed chats are a single message from a stranger. Agents get nudged toward this on their own. The server's instructions mention it, and so does every "no chat found" and "no matches" result, because that's the moment an agent is stuck on a name. link_status also says how many chats are unnamed. For agents, and people adapting this Everything below is for whoever is changing the code, including an AI agent working on a fork. The top of this file is for the person who owns the deployment. Shape of the thing MCP client ──OAuth──> Worker (src/index.ts) ├─ /mcp ───────────> mcp.ts: tools, one fresh McpServer per request ├─ /, /setup, /authorize, /login/*, /pair/* (landing, sign-in, pairing UI) └─ cron 04:00 UTC ──> pull() │ RPC v WhatsAppStore (Durable Object, one instance named "owner") ├─ SQLite: auth keys, messages, chats, contacts, FTS5 index └─ Baileys socket, opened only during a session (pairing or pull) │ WebSocket v WhatsApp servers One Durable Object holds everything. Its SQLite is both the Baileys auth store and the message store. The Worker is stateless apart from a KV namespace the OAuth provider uses for tokens and a short-lived "pending sign-in" record. Files File Job src/index.ts Routes. OAuth provider wiring, sign-in, pairing endpoints, /debug, cron. src/auth.ts Who counts as the owner. Two modes: a password, or a login service. Also the owner's browser session. src/phone.ts Phone numbers in any format to WhatsApp jids. src/whatsapp-do.ts The Durable Object. Runs Baileys sessions, exposes RPC methods for the Worker. src/store.ts Schema, ingesting Baileys events into SQLite, and every query. src/auth-state.ts Baileys auth state backed by the DO's SQLite (replaces useMultiFileAuthState). src/mcp.ts The MCP tools, and the hint that points agents at the naming tools. src/pages.ts, src/landing.ts, src/privacy.ts, src/tos.ts, src/ui.ts All HTML. ui.ts has the shared design tokens. src/ws-shim.ts, src/lru-shim.ts, src/polyfills.ts Make Baileys run on workerd. See below. scripts/vendor-bridge.mjs Extracts the wasm from Baileys' Rust bridge. Runs on npm install. local-export/ A separate one-off script that dumps your history to JSON files with no server. Not part of the deployment. How sessions work All WhatsApp traffic happens inside runSession() in whatsapp-do.ts. There are two kinds: Pairing (startPairing). No credentials yet. Baileys emits QR strings, kept in memory and rendered to SVG by pairStatus(). After a scan WhatsApp closes the socket with code 515 (restart required), and the session reconnects with the new credentials. History arrives as messaging-history.set chunks with a progress percentage. The session ends when WhatsApp reports 100 percent and the stream has been quiet for a few seconds. Pull (pull). Reconnects with stored credentials. WhatsApp delivers whatever it queued while we were away, then signals receivedPendingNotifications. The session ends after a few quiet seconds. Callers share one session: a second pull() while one is running gets the same promise. A pairing session outlives the HTTP request that started it. startPairing sets a Durable Object alarm, and alarm() awaits the session promise. An in-flight alarm handler is what keeps the object alive. The pairing page only watches; closing it doesn't stop the import. Idle detection counts only data events (messaging-history.set, messages.upsert), not connection chatter. Counting everything keeps a session open forever, because presence and contact updates never stop. Data model All in the Durable Object's SQLite (store.ts, initSchema). messages(chat, id, ts, from_me, sender, push_name, type, body), primary key (chat, id). Media is a label in body. Edits update body; deletes set it to [deleted]. messages_fts, an FTS5 index over body, kept in sync by triggers. chats(jid, name, is_group), contacts(jid, name, notify) and custom_names(jid, name). Display names resolve by WhatsApp's chat name, then WhatsApp's contact name, then a saved name, then the person's own push name, then the number. custom_names is what set_contact_names writes. alias(lid, pn). WhatsApp is moving to anonymous @lid identifiers. This maps them to phone-number JIDs, and addAlias folds existing rows into the phone-number form so one person is one chat. auth(k, v), Baileys credentials and signal keys, JSON with Buffers encoded by Baileys' BufferJSON. meta(k, v), currently just last_pull.