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

Let your AI agent learn your WhatsApp messages.
This is a small server that runs in your personal Cloudflare account. You hyperlink your cellphone to it as soon as, the way in which you’d hyperlink WhatsApp Web, and from then on any MCP shopper (Claude Code, Claude Desktop, something that speaks MCP) can search and browse your chats. It can learn and search your messages, and keep in mind names you give it for cellphone numbers. It cannot ship something.
Ask your agent issues like:
- “What did Dana say concerning the flights?”
- “Find each message the place somebody talked about the lease, throughout all my chats.”
- “Summarize what I missed within the household group this week.”
Your cellphone stays the supply of fact. The server is a linked gadget that your agent talks to.
It would not keep related to WhatsApp. When your agent asks for one thing, and as soon as a day, it connects, collects no matter WhatsApp queued for it since final time, writes that to its personal database, and hangs up. Your cellphone retains working usually and retains getting notifications.
Your messages stay in a SQLite database inside a Durable Object in your Cloudflare account. Nothing goes anyplace else.
You want a Cloudflare account on the Workers Paid plan (see the observe under), Node 20 or later, and your cellphone.
git clone https://github.com/finereli/whatsapp-mcp
cd whatsapp-mcp
npm set up
# 1. A spot 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 web page that hyperlinks your cellphone. 12+ characters.
npx wrangler secret put OWNER_PASSWORD
# 3. Deploy.
npm run deploy
Wrangler prints your URL, one thing like https://whatsapp-mcp.your-name.workers.dev. Open it. The entrance web page has the 2 remaining steps:
-
Link WhatsApp. Scan the QR code with WhatsApp (Settings, Linked Devices, Link a Device). If you are on the cellphone itself and may’t scan a display, use the cellphone quantity possibility as a substitute. The first hyperlink imports your historical past. The web page reveals progress, and you may shut it.
-
Connect your agent. In Claude Code:
claude mcp add --transport http whatsapp https://your-url/mcp
Then sort
/mcpand check in with the password. Other shoppers: level them on the similar/mcpURL.
If you need your personal area, see the feedback on the backside of wrangler.jsonc.
Importing a big historical past decodes numerous knowledge in a single go, and the default CPU restrict on Workers is just too small for it. wrangler.jsonc raises it, and elevating it wants the paid plan, which is a number of {dollars} a month. I have not tried the free plan. If you are on it, take away the limits block and count on the primary import to be fragile.
This is not official. WhatsApp has no API for studying your personal private messages. This makes use of the identical protocol WhatsApp Web makes use of, by way of an open supply library (Baileys). That’s towards WhatsApp’s phrases. For a linked gadget that solely reads, the sensible danger is low, nevertheless it is not zero. Use it by yourself account, and do not use it to ship messages.
You get what WhatsApp sends. The first hyperlink imports no matter historical past WhatsApp decides at hand a brand new gadget. It might not be every thing. It solely does this as soon as per hyperlink. To import once more, relink (your saved messages are stored, and a re-import fills in something lacking).
Some chats present up as cellphone numbers. WhatsApp would not give a linked gadget each contact identify, and most of those are one-off messages from individuals you by no means saved. For those that matter, your agent can repair it: it lists the unnamed chats, seems to be the numbers up in your contacts with no matter contacts device it already has (a Google Contacts or Gmail connector, for instance), and saves the names. The server by no means connects to a contacts service itself. See “Naming chats” under.
Media is not downloaded. Photos, voice notes and paperwork seem as labels like [image] or [voice note 12s], with the caption if there was one. Nothing is transcribed.
It’s for one particular person. One deployment is one WhatsApp account and one proprietor. That’s deliberate: every deployment holds a full message historical past and a stay login to the account, so everybody ought to run their very own.
Relinking. If your cellphone says the gadget was eliminated, otherwise you need to begin over, ask your agent to run relink, or open /setup and select “Unlink and scan once more”. You scan a brand new QR code. Stored messages are stored.
| Tool | What it does |
|---|---|
list_chats |
Chats, most up-to-date first. Filter by identify or quantity. |
get_messages |
Read one chat. Page again or ahead by date. |
search_messages |
Full-text search throughout every thing, or inside one chat. |
unnamed_chats |
Chats recognized solely by cellphone quantity, busiest first, with the very last thing the opposite particular person stated. |
set_contact_names |
Save names for cellphone numbers, in any format. Names stick with the quantity, so additionally they present up in teams. |
sync_now |
Pull new messages proper now. |
link_status |
Is it linked, when did it final pull, how a lot is saved. |
relink |
Unlink and begin over. A human has to scan the brand new QR code. |
Every learn device first tops up from WhatsApp if the saved copy is greater than ten minutes previous.
WhatsApp solely offers this server the names it is aware of. The relaxation present up as cellphone numbers. Rather than have the server log in to a contacts service (which implies a cloud challenge, consent screens and, for Google, an app overview), it palms the job to your agent, which normally already has a option to attain your contacts.
Say “discover names for my unnamed chats”. The agent calls unnamed_chats, matches the numbers towards your contacts, and saves what it finds with set_contact_names. It will not discover everybody: most unnamed chats are a single message from a stranger.
Agents get nudged towards this on their very own. The server’s directions point out it, and so does each “no chat discovered” and “no matches” end result, as a result of that is the second an agent is caught on a reputation. link_status additionally says what number of chats are unnamed.
Everything under is for whoever is altering the code, together with an AI agent engaged on a fork. The prime of this file is for the one who owns the deployment.
MCP shopper ──OAuth──> Worker (src/index.ts)
├─ /mcp ───────────> mcp.ts: instruments, one contemporary McpServer per request
├─ /, /setup, /authorize, /login/*, /pair/* (touchdown, sign-in, pairing UI)
└─ cron 04:00 UTC ──> pull()
│ RPC
v
WhatsAppStore (Durable Object, one occasion named "proprietor")
├─ SQLite: auth keys, messages, chats, contacts, FTS5 index
└─ Baileys socket, opened solely throughout a session (pairing or pull)
│ WebSocket
v
WhatsApp servers
One Durable Object holds every thing. Its SQLite is each the Baileys auth retailer and the message retailer. The Worker is stateless aside from a KV namespace the OAuth supplier makes use of for tokens and a short-lived “pending sign-in” document.
| File | Job |
|---|---|
src/index.ts |
Routes. OAuth supplier wiring, sign-in, pairing endpoints, /debug, cron. |
src/auth.ts |
Who counts because the proprietor. Two modes: a password, or a login service. Also the proprietor’s browser session. |
src/cellphone.ts |
Phone numbers in any format to WhatsApp jids. |
src/whatsapp-do.ts |
The Durable Object. Runs Baileys classes, exposes RPC strategies for the Worker. |
src/retailer.ts |
Schema, ingesting Baileys occasions into SQLite, and each question. |
src/auth-state.ts |
Baileys auth state backed by the DO’s SQLite (replaces useMultiFileAuthState). |
src/mcp.ts |
The MCP instruments, and the trace that factors brokers on the naming instruments. |
src/pages.ts, src/touchdown.ts, src/privateness.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 under. |
scripts/vendor-bridge.mjs |
Extracts the wasm from Baileys’ Rust bridge. Runs on npm set up. |
local-export/ |
A separate one-off script that dumps your historical past to JSON information with no server. Not a part of the deployment. |
All WhatsApp visitors occurs inside runSession() in whatsapp-do.ts. There are two sorts:
- Pairing (
beginPairing). No credentials but. Baileys emits QR strings, stored in reminiscence and rendered to SVG bypairStatus(). After a scan WhatsApp closes the socket with code 515 (restart required), and the session reconnects with the brand new credentials. History arrives asmessaging-history.setchunks with aprogressshare. The session ends when WhatsApp studies 100% and the stream has been quiet for a number of seconds. - Pull (
pull). Reconnects with saved credentials. WhatsApp delivers no matter it queued whereas we had been away, then alertsreceivedPendingNotifications. The session ends after a number of quiet seconds.
Callers share one session: a second pull() whereas one is operating will get the identical promise.
A pairing session outlives the HTTP request that began it. beginPairing units a Durable Object alarm, and alarm() awaits the session promise. An in-flight alarm handler is what retains the thing alive. The pairing web page solely watches; closing it would not cease the import.
Idle detection counts solely knowledge occasions (messaging-history.set, messages.upsert), not connection chatter. Counting every thing retains a session open ceaselessly, as a result of presence and phone updates by no means cease.
All within the Durable Object’s SQLite (retailer.ts, initSchema).
messages(chat, id, ts, from_me, sender, push_name, sort, physique), major key(chat, id). Media is a label inphysique. Edits replacephysique; deletes set it to[deleted].messages_fts, an FTS5 index overphysique, stored in sync by triggers.chats(jid, identify, is_group),contacts(jid, identify, notify)andcustom_names(jid, identify). Display names resolve by WhatsApp’s chat identify, then WhatsApp’s contact identify, then a saved identify, then the particular person’s personal push identify, then the quantity.custom_namesis whatset_contact_nameswrites.alias(lid, pn). WhatsApp is transferring to nameless@lididentifiers. This maps them to phone-number JIDs, andaddAliasfolds present rows into the phone-number kind so one particular person is one chat.auth(okay, v), Baileys credentials and sign keys, JSON with Buffers encoded by Baileys’BufferJSON.meta(okay, v), presently simplylast_pull.
Ingest is idempotent. Re-importing historical past upserts: it fills in a lacking sender or push_name and by no means duplicates. That is what makes relinking secure.
These all value actual time to seek out. Read them earlier than touching the Baileys components.
- Baileys wants three shims to run on workerd.
ws: the bundle wants Node sockets.ws-shim.tswraps the Workers WebSocket, opened withfetch()and anUpgradeheader as a result of that is the one option to set theOriginheader WhatsApp checks.whatsapp-rust-bridge: it ships its wasm as base64 and compiles it at runtime, which workerd forbids.vendor-bridge.mjswrites the wasm to a file and patches the loader to import it as aCompiledWasmmodule. Thevendor/folder is generated, not dedicated.- Timers: Node timers have
.unref()and workerd’s do not.polyfills.tsprovides it. It should be the primary import in each the Worker and the Durable Object module.
lru-cacheautopurge breaks the primary import. Baileys creates caches withttlAutopurge: true, which begins onesetTimeoutper entry. workerd permits 10,000 pending timers per object, and the primary historical past sync blows by way of that withQuotaExceededError.lru-shim.tsturns autopurge off. Expired entries are nonetheless dropped lazily on entry.- QR pairing by no means units
creds.registered. That flag belongs to the cell registration circulation. “Is this linked” iscreds.me?.id. - History comes as soon as per hyperlink. If a primary import fails midway, that historical past is gone till you relink. Don’t ship a change to ingest code with out testing it towards an actual first hyperlink.
- Group senders arrive in two locations. Live messages have
key.participant. History messages put it onmsg.participant. Read each. - Pin Baileys precisely. It’s a prerelease and its API strikes between launch candidates.
- Deploys do not evict a busy Durable Object. An object with an energetic session retains operating previous code till the session ends. Renaming the category with a migration did not evict it both. If you’ve got shipped a repair and the thing nonetheless misbehaves, cease sending it requests and wait three to 5 minutes. Every probe, together with
/debug, resets its idle timer, so polling a stale object retains it stale. Baileys additionally leaks interval timers when a socket closes;end()inwhatsapp-do.tsclears them so the thing can go idle in any respect. - Logging out takes two steps.
logout()first connects and calls Baileys’logout(), so the gadget disappears from the cellphone’s listing, then clears native credentials. If step one fails it says so within the debug occasions, and the person has to take away the gadget by hand. - WhatsApp shops a placeholder the place a reputation needs to be. Chats it has no contact for are named after the quantity, typically masked (
+972∙∙∙∙∙∙∙20), within the chat identify and the contact identify.numberLikeinretailer.tstreats these as no identify, and ingest refuses to retailer them. Before that repair a placeholder outranked an actual saved identify. - Don’t name
markOnlineOnConnect. It’s set to false on goal. Marking the linked gadget on-line makes the cellphone cease displaying notifications.
| Name | Kind | Purpose |
|---|---|---|
OWNER_PASSWORD |
secret | Password mode (default). 12+ characters or sign-in refuses. |
OWNER_EMAIL |
var | Login-service mode: the one e mail allowed in. |
OPERATOR_NAME |
var, non-compulsory | Shown on the privateness web page as who runs this server. |
DEFAULT_COUNTRY_CODE |
var, non-compulsory | Country code (digits, no plus), used when your agent saves a quantity written with out one, e.g. 972. |
LOGIN_ORIGIN |
var | Set this to change to login-service mode. |
ALLOWED_ORIGINS |
var | Login-service mode: comma-separated origins whose tokens are accepted. Production ought to listing solely manufacturing. |
PIN |
secret, non-compulsory | 32+ random characters. Enables the /debug routes by way of an x-pin header. Leave it unset to go away them off. |
OAUTH_KV |
binding | Token retailer for the OAuth supplier and pending sign-ins. |
WA |
binding | The Durable Object. |
Keep a deployment’s personal values (area, KV id, e mail) in wrangler.native.jsonc, which is gitignored, and deploy with npm run deploy:native. wrangler.jsonc is the template different individuals begin from.
- Add a device. Register it in
mcp.tsand put the SQL inretailer.ts. Read instruments ought to namecontemporary()first. Keep instruments read-only until you imply in any other case. If a failure leaves an agent caught, say within the device end result what to attempt subsequent, the way in whichNAME_HINTdoes for lacking names. - Change who can check in.
auth.tspicks a mode (authMode) and the routes inindex.tscomply with it. To add one other identification supplier, add a mode there and a callback route. The remainder of the code solely sees an opaque bearer token. - Make it multi-user. Today the Durable Object is addressed by the mounted identify
proprietor(wa()inindex.ts,handleMcpinmcp.ts), anduserIdis the fixed'proprietor'within the OAuth props. To go multi-user, derive each from the signed-in identification and key the thing on it. Think about whether or not you need to maintain different individuals’s message historical past first. - Other methods to fill in names. Anything that may produce
{ jid, identify }rows can nameretailer.setCustomNames. A CSV add, a CardDAV pull or a scheduled job would all work. The agent route exists so the server wants no credentials for any of them. - Sending messages. Not constructed, on goal. Think arduous earlier than including it.
With PIN set, these return JSON and want the header x-pin: :
/debug: session section, linked or not, counts, and the final occasions from the session (connection modifications, historical past chunk sizes, errors)./debug/pull: run a pull now./debug/chats?q=,/debug/messages?chat=,/debug/search?q=,/debug/sorts: learn the shop with out going by way of MCP./debug/unnamed: the chatsunnamed_chatswould return./debug/sql?q=: one read-onlySELECTtowards the shop, for locating out why a reputation or a depend seems to be incorrect./debug/logout: the identical asrelink.
Local growth: npm run dev with a .dev.vars containing OWNER_PASSWORD=... (12+ characters). It runs a neighborhood Durable Object with its personal empty storage, so you may train the sign-in circulation and see an actual QR code, however linking a cellphone from native dev makes a second linked gadget.
To check the pairing UI states with out a cellphone, serve the web page with a mocked /pair/standing. Everything the web page wants is that one JSON form: paired, operating, progress, qrSvg, messages, chats.
npm run typecheck, thennpm check. The check runs the true retailer code towards Node’s built-in SQLite (Node 22.13 or later), together with the migration from older variations. That SQLite has no FTS5, so search itself is not coated.- Deploy, then
GET /debugwith the PIN. It ought to come again withsection: "idle"andpaired: true. GET /debug/pull. A quiet account ought to returnmotive: "idle"in about ten seconds.- Call
list_chats, thenget_messageson a Hebrew or emoji-heavy chat, thensearch_messages. Thenunnamed_chats, andset_contact_nameson one quantity, and ensure the chat reveals the identify. - If you touched ingest, relink and ensure the saved message depend would not develop (re-import is idempotent) and that group messages present actual sender names.
- Chats with no identify in WhatsApp and none saved present as cellphone numbers. Nothing refreshes names by itself: they solely get saved when an agent (otherwise you) does it.
- If the each day pull finds the session logged out, nothing tells you.
link_statusreveals it the subsequent time your agent asks. - Edits and deletes are utilized after they arrive as protocol messages. That path hasn’t been examined towards actual ones.
- It’s solely been exercised towards one actual account.
MIT
