Show HN: WhatsApp MCP running free on Cloudflare

Hacker News by 15 min read 132x views
Show HN: WhatsApp MCP running free on Cloudflare

Share Post

Let your AI delegate peruse your WhatsApp messages.

This is a small server that runs in your own Cloudflare account. You nexus your phone to it once, the way you'd nexus WhatsApp Web, and from afterward on any MCP client (Claude Code, Claude Desktop, item that speaks MCP) can hunt and peruse your chats. It can peruse and hunt your messages, and recall names you provision it for phone numbers. It can't dispatch anything.

Ask your delegate things like:

  • "What did Dana say concerning the flights?"
  • "Find all communication anywhere person mentioned the lease, throughout all my chats."
  • "Summarize what I missed in the family collection this week."

Your phone stays the origin of truth. The server is a connected equipment that your delegate talks to.

It doesn't remain connected to WhatsApp. When your delegate asks for something, and formerly a day, it connects, collects any WhatsApp queued for it since final time, writes that to its own database, and hangs up. Your phone keeps operating normally and keeps getting notifications.

Your messages live in a SQLite repository inner a Durable Object in your Cloudflare account. Nothing goes anyplace else.

You need a Cloudflare document on the Workers Paid scheme (see the note below), Node 20 or later, and your phone.

git copy https://github.com/finereli/whatsapp-mcp cd whatsapp-mcp npm install # 1. A location 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 leaf that links your phone. 12+ characters. npx wrangler concealed put OWNER_PASSWORD # 3. Deploy. npm run deploy

Wrangler prints your URL, item akin https://whatsapp-mcp.your-name.workers.dev. Open it. The forefront leaf has the two remaining steps:

  1. Link WhatsApp. Scan the QR code alongside WhatsApp (Settings, Linked Devices, Link a Device). If you're on the phone itself and can't scan a screen, use the phone figure choice instead. The archetypal nexus imports your history. The leaf shows progress, and you can near it.

  2. Connect your agent. In Claude Code:

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

    Then category /mcp and sign in alongside the password. Other clients: item them at the identical /mcp URL.

If you desire your own domain, see the comments at the base of wrangler.jsonc.

Importing a ample former decodes a lot of data in one go, and the default CPU bounds 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 liberated plan. If you're on it, eliminate the limits obstacle and anticipate the archetypal import to be fragile.

This isn't official. WhatsApp has no API for study your own individual messages. This uses the identical protocol WhatsApp Web uses, through an open origin archive (Baileys). That's against WhatsApp's terms. For a connected equipment that lone reads, the applicable hazard is low, but it isn't zero. Use it on your own account, and don't use it to dispatch messages.

You get what WhatsApp sends. The archetypal nexus imports any former WhatsApp decides to hand a new device. It may not be everything. It lone does this formerly per link. To import again, relink (your stored messages are kept, and a re-import fills in item missing).

Some chats display up as phone numbers. WhatsApp doesn't provision a connected equipment all communication name, and most of these are one-off messages from group you never saved. For the ones that matter, your delegate can fix it: it lists the unnamed chats, looks the numbers up in your contacts alongside any contacts tool it already has (a Google Contacts or Gmail connector, for example), and saves the names. The server never connects to a contacts assistance itself. See "Naming chats" below.

Media isn't downloaded. Photos, sound notes and documents appear as labels akin [image] or [voice note 12s], alongside the caption if there was one. Nothing is transcribed.

It's for one person. One deployment is one WhatsApp document and one owner. That's deliberate: all deployment holds a complete communication former and a live login to the account, so everyone should run their own.

Relinking. If your phone says the equipment was removed, or you desire to commencement over, ask your delegate to run relink, or open /setup and choose "Unlink and scan again". You scan a new QR code. Stored messages are kept.

Tool What it does
list_chats Chats, most latest first. Filter by name or number.
get_messages Read one chat. Page rear or onward by date.
search_messages Full-text hunt throughout everything, or inside one chat.
unnamed_chats Chats known lone by phone number, busiest first, alongside the final item the another individual said.
set_contact_names Save names for phone numbers, in any format. Names rod to the number, so they additionally display up in groups.
sync_now Pull new messages correct now.
link_status Is it linked, whenever did it final pull, how much is stored.
relink Unlink and commencement over. A individual has to scan the new QR code.

Every peruse tool archetypal tops up from WhatsApp if the stored copy is additional than ten minutes old.

WhatsApp lone gives this server the names it knows. The remainder display up as phone numbers. Rather than have the server log in to a contacts assistance (which method a haze project, consent screens and, for Google, an app review), it hands the job to your agent, which normally already has a way to attain your contacts.

Say "find names for my unnamed chats". The delegate calls unnamed_chats, matches the numbers against your contacts, and saves what it finds alongside set_contact_names. It won't discover everyone: most unnamed chats are a sole communication from a stranger.

Agents get nudged toward this on their own. The server's instructions citation it, and so does all "no conversation found" and "no matches" result, since that's the instant an delegate is stuck on a name. link_status additionally says how many chats are unnamed.


Everything below is for whoever is changing the code, including an AI delegate operating on a fork. The top of this document is for the individual who owns the deployment.

MCP client ──OAuth──> Worker (src/index.ts) ├─ /mcp ───────────> mcp.ts: tools, one caller McpServer per request ├─ /, /setup, /authorize, /login/*, /pair/* (landing, sign-in, pairing UI) └─ cron 04:00 UTC ──> pull() │ RPC v WhatsAppStore (Durable Object, one case named "owner") ├─ SQLite: auth keys, messages, chats, contacts, FTS5 index └─ Baileys socket, opened lone during a meeting (pairing or pull) │ WebSocket v WhatsApp servers 

One Durable Object holds everything. Its SQLite is the two the Baileys auth shop and the communication store. The Worker is homeless distinct from a KV namespace the OAuth provider uses for tokens and a short-lived "pending sign-in" record.

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 all query.
src/auth-state.ts Baileys auth province backed by the DO's SQLite (replaces useMultiFileAuthState).
src/mcp.ts The MCP tools, and the clue 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 scheme 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 distinct one-off manuscript that dumps your former to JSON records alongside no server. Not part of the deployment.

All WhatsApp traffic happens inner runSession() in whatsapp-do.ts. There are two kinds:

  • Pairing (startPairing). No credentials yet. Baileys emits QR strings, kept in recollection and rendered to SVG by pairStatus(). After a scan WhatsApp closes the socket alongside code 515 (restart required), and the meeting reconnects alongside the new credentials. History arrives as messaging-history.set chunks alongside a advancement percentage. The meeting ends whenever WhatsApp reports 100 percent and the stream has been quiescent for a few seconds.
  • Pull (pull). Reconnects alongside stored credentials. WhatsApp delivers any it queued during we were away, afterward signals receivedPendingNotifications. The meeting ends following a few quiescent seconds.

Callers portion one session: a second pull() during one is operating gets the identical promise.

A pairing meeting outlives the HTTP petition that started it. startPairing sets a Durable Object alarm, and alarm() awaits the meeting promise. An in-flight alert handler is what keeps the entity alive. The pairing leaf lone watches; decision it doesn't halt the import.

Idle finding counts lone data events (messaging-history.set, messages.upsert), not association chatter. Counting everything keeps a meeting open forever, since existence and communication updates never stop.

All in the Durable Object's SQLite (store.ts, initSchema).

  • messages(chat, id, ts, from_me, sender, push_name, type, body), chief key (chat, id). Media is a tag in body. Edits update body; deletes set it to [deleted].
  • messages_fts, an FTS5 indicator complete body, kept in sync by triggers.
  • chats(jid, name, is_group), contacts(jid, name, notify) and custom_names(jid, name). Display names determine by WhatsApp's conversation name, afterward WhatsApp's communication name, afterward a saved name, afterward the person's own shove name, afterward 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 individual is one chat.
  • auth(k, v), Baileys credentials and indication keys, JSON alongside Buffers encoded by Baileys' BufferJSON.
  • meta(k, v), currently fair last_pull.

Ingest is idempotent. Re-importing former upserts: it fills in a missing sender or push_name and never duplicates. That is what makes relinking safe.

What to cognize before changing things

These all disbursal genuine period to find. Read them before touching the Baileys parts.

  1. Baileys needs three shims to run on workerd.
    • ws: the bundle needs Node sockets. ws-shim.ts wraps the Workers WebSocket, opened alongside fetch() and an Upgrade header since that's the lone way 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 document and patches the loader to import it as a CompiledWasm module. The vendor/ directory is generated, not committed.
    • Timers: Node timers have .unref() and workerd's don't. polyfills.ts adds it. It must be the archetypal import in the two the Worker and the Durable Object module.
  2. lru-cache autopurge breaks the archetypal import. Baileys creates caches alongside ttlAutopurge: true, which starts one setTimeout per entry. workerd allows 10,000 pending timers per object, and the archetypal former sync blows through that alongside QuotaExceededError. lru-shim.ts turns autopurge off. Expired entries are motionless dropped lazily on access.
  3. QR pairing never sets creds.registered. That emblem belongs to the mobile registration flow. "Is this linked" is creds.me?.id.
  4. History comes formerly per link. If a archetypal import fails halfway, that former is gone until you relink. Don't container a alter to ingest code without evaluation it against a genuine archetypal link.
  5. Group senders attain in two places. Live messages have key.participant. History messages put it on msg.participant. Read both.
  6. Pin Baileys exactly. It's a prerelease and its API moves between publish candidates.
  7. Deploys don't evict a occupied Durable Object. An entity alongside an energetic meeting keeps operating old code until the meeting ends. Renaming the category alongside a immigration didn't evict it either. If you've shipped a fix and the entity motionless misbehaves, halt sending it requests and delay three to five minutes. Every probe, including /debug, resets its idle timer, so polling a old entity keeps it stale. Baileys additionally leaks duration timers whenever a socket closes; finish() in whatsapp-do.ts clears them so the entity can go idle at all.
  8. Logging out takes two steps. logout() archetypal connects and calls Baileys' logout(), so the equipment disappears from the phone's list, afterward clears local credentials. If the archetypal stage fails it says so in the debug events, and the person has to eliminate the equipment by hand.
  9. WhatsApp stores a placeholder anywhere a name should be. Chats it has no communication for are named following the number, sometimes masked (+972∙∙∙∙∙∙∙20), in the conversation name and the communication name. numberLike in store.ts treats those as no name, and ingest refuses to shop them. Before that fix a placeholder outranked a genuine saved name.
  10. Don't call markOnlineOnConnect. It's set to false on purpose. Marking the connected equipment online makes the phone halt showing notifications.
Name Kind Purpose
OWNER_PASSWORD secret Password manner (default). 12+ characters or sign-in refuses.
OWNER_EMAIL var Login-service mode: the one email allowed in.
OPERATOR_NAME var, optional Shown on the privacy leaf as who runs this server.
DEFAULT_COUNTRY_CODE var, optional Country code (digits, no plus), used whenever your delegate saves a figure written without one, e.g. 972.
LOGIN_ORIGIN var Set this to toggle to login-service mode.
ALLOWED_ORIGINS var Login-service mode: comma-separated origins whose tokens are accepted. Production should catalog lone production.
PIN secret, optional 32+ random characters. Enables the /debug routes via an x-pin header. Leave it unset to depart them off.
OAUTH_KV binding Token shop for the OAuth provider and pending sign-ins.
WA binding The Durable Object.

Keep a deployment's own values (domain, KV id, email) in wrangler.local.jsonc, which is gitignored, and deploy alongside npm run deploy:local. wrangler.jsonc is the template another group commencement from.

  • Add a tool. Register it in mcp.ts and put the SQL in store.ts. Read tools should call fresh() first. Keep tools read-only unless you average otherwise. If a nonaccomplishment leaves an delegate stuck, say in the tool outcome what to try next, the way NAME_HINT does for missing names.
  • Change who can sign in. auth.ts picks a manner (authMode) and the routes in index.ts prosecute it. To add another character provider, add a manner there and a recall route. The remainder of the code lone sees an opaque transporter token.
  • Make it multi-user. Today the Durable Object is addressed by the fixed name owner (wa() in index.ts, handleMcp in mcp.ts), and userId is the changeless 'owner' in the OAuth props. To go multi-user, get the two from the signed-in character and key the entity on it. Think concerning whether you desire to clasp another people's communication former first.
  • Other ways to inhabit in names. Anything that can create { jid, name } rows can call store.setCustomNames. A CSV upload, a CardDAV drag or a scheduled job would all work. The delegate path exists so the server needs no credentials for any of them.
  • Sending messages. Not built, on purpose. Think difficult before adding it.

With PIN set, these come back JSON and need the header x-pin: <PIN>:

  • /debug: meeting phase, connected or not, counts, and the final events from the meeting (connection changes, former chunk sizes, errors).
  • /debug/pull: run a drag now.
  • /debug/chats?q=, /debug/messages?chat=, /debug/search?q=, /debug/types: peruse the shop without going through MCP.
  • /debug/unnamed: the chats unnamed_chats would return.
  • /debug/sql?q=: one read-only SELECT against the store, for finding out why a name or a figure looks wrong.
  • /debug/logout: the identical as relink.

Local development: npm run dev alongside a .dev.vars containing OWNER_PASSWORD=... (12+ characters). It runs a local Durable Object alongside its own bare storage, so you can exercise the sign-in stream and see a genuine QR code, but linking a phone from local dev makes a second connected device.

To test the pairing UI states without a phone, assist the leaf alongside a mocked /pair/status. Everything the leaf needs is that one JSON shape: paired, running, progress, qrSvg, messages, chats.

  1. npm run typecheck, afterward npm test. The test runs the genuine shop code against Node's built-in SQLite (Node 22.13 or later), including the immigration from older versions. That SQLite has no FTS5, so hunt itself isn't covered.
  2. Deploy, afterward GET /debug alongside the PIN. It should arrive rear alongside phase: "idle" and paired: true.
  3. GET /debug/pull. A quiescent document should come back reason: "idle" in concerning ten seconds.
  4. Call list_chats, afterward get_messages on a Hebrew or emoji-heavy chat, afterward search_messages. Then unnamed_chats, and set_contact_names on one number, and verify the conversation shows the name.
  5. If you touched ingest, relink and verify the stored communication figure doesn't develop (re-import is idempotent) and that collection messages display genuine sender names.
  • Chats alongside no name in WhatsApp and none saved display as phone numbers. Nothing refreshes names by itself: they lone get saved whenever an delegate (or you) does it.
  • If the regular drag finds the meeting logged out, nothing tells you. link_status shows it the next period your delegate asks.
  • Edits and deletes are applied whenever they attain as protocol messages. That way hasn't been tested against genuine ones.
  • It's lone been exercised against one genuine account.

MIT

Other Article Hacker News
↑
Close Right Ads
Close Left Ads