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:

  1. 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.

  2. Connect your agent. In Claude Code:

    claude mcp add --transport http whatsapp https://your-url/mcp

    Then sort /mcp and check in with the password. Other shoppers: level them on the similar /mcp URL.

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 by pairStatus(). After a scan WhatsApp closes the socket with code 515 (restart required), and the session reconnects with the brand new credentials. History arrives as messaging-history.set chunks with a progress share. 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 alerts receivedPendingNotifications. 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 in physique. Edits replace physique; deletes set it to [deleted].
  • messages_fts, an FTS5 index over physique, stored in sync by triggers.
  • chats(jid, identify, is_group), contacts(jid, identify, notify) and custom_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_names is what set_contact_names writes.
  • alias(lid, pn). WhatsApp is transferring to nameless @lid identifiers. This maps them to phone-number JIDs, and addAlias folds 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 simply last_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.

What to know earlier than altering issues

These all value actual time to seek out. Read them earlier than touching the Baileys components.

  1. Baileys wants three shims to run on workerd.
    • ws: the bundle wants Node sockets. ws-shim.ts wraps the Workers WebSocket, opened with fetch() and an Upgrade header as a result of that is the one option to set the Origin header WhatsApp checks.
    • whatsapp-rust-bridge: it ships its wasm as base64 and compiles it at runtime, which workerd forbids. vendor-bridge.mjs writes the wasm to a file and patches the loader to import it as a CompiledWasm module. The vendor/ folder is generated, not dedicated.
    • Timers: Node timers have .unref() and workerd’s do not. polyfills.ts provides it. It should be the primary import in each the Worker and the Durable Object module.
  2. lru-cache autopurge breaks the primary import. Baileys creates caches with ttlAutopurge: true, which begins one setTimeout per entry. workerd permits 10,000 pending timers per object, and the primary historical past sync blows by way of that with QuotaExceededError. lru-shim.ts turns autopurge off. Expired entries are nonetheless dropped lazily on entry.
  3. QR pairing by no means units creds.registered. That flag belongs to the cell registration circulation. “Is this linked” is creds.me?.id.
  4. 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.
  5. Group senders arrive in two locations. Live messages have key.participant. History messages put it on msg.participant. Read each.
  6. Pin Baileys precisely. It’s a prerelease and its API strikes between launch candidates.
  7. 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() in whatsapp-do.ts clears them so the thing can go idle in any respect.
  8. 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.
  9. 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. numberLike in retailer.ts treats these as no identify, and ingest refuses to retailer them. Before that repair a placeholder outranked an actual saved identify.
  10. 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.ts and put the SQL in retailer.ts. Read instruments ought to name contemporary() 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 which NAME_HINT does for lacking names.
  • Change who can check in. auth.ts picks a mode (authMode) and the routes in index.ts comply 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() in index.ts, handleMcp in mcp.ts), and userId is 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 name retailer.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 chats unnamed_chats would return.
  • /debug/sql?q=: one read-only SELECT towards the shop, for locating out why a reputation or a depend seems to be incorrect.
  • /debug/logout: the identical as relink.

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.

  1. npm run typecheck, then npm 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.
  2. Deploy, then GET /debug with the PIN. It ought to come again with section: "idle" and paired: true.
  3. GET /debug/pull. A quiet account ought to return motive: "idle" in about ten seconds.
  4. Call list_chats, then get_messages on a Hebrew or emoji-heavy chat, then search_messages. Then unnamed_chats, and set_contact_names on one quantity, and ensure the chat reveals the identify.
  5. 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_status reveals 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



Source link