跳到正文
Hacker News (official RSS)(github.com)·· 3 小時前精選AI 評分69

Show HN:WhatsApp MCP 可在 Cloudflare 免費執行

Show HN: WhatsApp MCP running free on Cloudflare

AI 導讀

此專案提供一個在 Cloudflare Workers 上執行的 WhatsApp MCP 伺服器,允許 AI 代理讀取並搜尋使用者訊息,並以免費方式部署。

  • \n- 部署環境:需 Cloudflare Workers 付費方案、Node 20+ 及手機。
  • 部署後 URL 形如 https://whatsapp-mcp.your-name.workers.dev。
推薦理由

此專案示範如何在 Cloudflare Workers 上以免費方式實作 WhatsApp MCP,提供 AI 代理可讀取並搜尋訊息的實用方法,並說明部署流程與安全注意事項,確保使用者能快速上手並維護資料隱私。

正文 · AI 翻譯

讓你的 AI 代理閱讀你的 WhatsApp 訊息。

這是一個在你自己的 Cloudflare 帳戶中執行的小型伺服器。你只需像連結 WhatsApp Web 那樣將手機一次性連結到它,之後任何 MCP 客戶端(Claude Code、Claude Desktop 或任何支援 MCP 的程式)都能搜尋並閱讀你的聊天。它能閱讀並搜尋訊息,並記住你為電話號碼指定的名稱,但無法傳送任何訊息。

向你的代理詢問類似以下的問題:

  • "Dana 對航班說了什麼?"
  • "找出所有聊天中有人提到租約的訊息。"
  • "總結本週我在家庭群組中錯過的內容。"

工作原理

你的手機仍是資訊的唯一真實來源。伺服器則是代理溝通的連結裝置。

它不會持續連線到 WhatsApp。當你的代理請求資料,或每天一次時,它會連線,收集自上一次以來 WhatsApp 為其排隊的訊息,將其寫入自己的資料庫,然後結束通話。你的手機仍能正常運作並接收通知。

你的訊息存於你 Cloudflare 帳戶中的 Durable Object 內的 SQLite 資料庫。資訊不會傳送到其他地方。

設定

你需要一個使用 Workers 付費方案的 Cloudflare 帳戶(請參閱下方說明)、Node 20 或更新版本,以及手機。

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 會輸出你的 URL,類似 https://whatsapp-mcp.your-name.workers.dev。開啟它。首頁包含剩餘的兩個步驟:

  1. 連結 WhatsApp。 使用 WhatsApp 掃描 QR 碼(設定 → 已連結裝置 → 連結裝置)。如果你直接在手機上操作且無法掃描螢幕,請改用電話號碼選項。第一次連結會匯入你的歷史訊息。頁面會顯示進度,完成後可關閉。

  2. 連結你的代理。 在 Claude Code:

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

    接著輸入 /mcp 並以密碼登入。其他客戶端:將它們指向相同的 /mcp URL。

若想使用自己的網域,請參閱 wrangler.jsonc 之底部說明。

Workers 付費方案說明

匯入大量歷史訊息一次性解碼大量資料,而 Workers 的預設 CPU 限制不足以支援。wrangler.jsonc 可提升此限制,且提升需要付費方案,費用僅數美元。本人尚未嘗試免費方案;若你使用免費方案,請移除 limits 區塊,並預期首次匯入可能不穩定。

須知事項

這並非官方功能。 WhatsApp 沒有提供讀取你個人訊息的 API。此方案使用與 WhatsApp Web 相同的協定,透過開源函式庫 (Baileys)。這違反 WhatsApp 的服務條款。對於僅閱讀的連結裝置,實際風險低,但並非零。請僅於自用帳戶使用,並勿用於傳送訊息。

你只能收到 WhatsApp 傳送的內容。 第一次連結時會匯入 WhatsApp 為新裝置決定的歷史訊息,可能不完整。此操作僅在每次連結時執行一次。若需再次匯入,請重新連結(已儲存的訊息會保留,重新匯入會補齊遺漏)。

部分聊天顯示為電話號碼。 WhatsApp 連結裝置無法取得所有聯絡人名稱,且大多為你未儲存的單次訊息。對於重要的聊天,代理可以修正:它會列出未命名的聊天,並使用已有的聯絡人工具(如 Google Contacts 或 Gmail 聯結器)查詢號碼,然後儲存名稱。伺服器不會自行連線至聯絡人服務。請參閱下方"命名聊天"說明。

媒體不會下載。 照片、語音備忘錄及檔案會以 [image] 或 [voice note 12s] 等標籤顯示,若有說明文字則會附上。內容不會被轉錄。

這是為單一人設計。 一次部署即一個 WhatsApp 帳號與一位擁有者。這是刻意的:每次部署都保有完整訊息歷史與帳號的即時登入,所有人都應自行執行。

重新連結。 若手機顯示裝置已被移除,或你想重新開始,請讓你的代理執行 relink,或開啟 /setup 並選擇 "Unlink and scan again"。你將掃描新的 QR 碼。已儲存的訊息會保留。

代理能做的事

工具 功能
list_chats 聊天,最近的先顯示。可依名稱或號碼篩選。
get_messages 閱讀單一聊天。可按日期往前或往後翻頁。
search_messages 全文搜尋整體或單一聊天。
unnamed_chats 僅以電話號碼識別的聊天,按繁忙度排序,並顯示對方最近的訊息。
set_contact_names 為電話號碼儲存名稱,格式不限。名稱會繫結於號碼,亦會在群組中顯示。
sync_now 立即拉取新訊息。
link_status 是否已連結、最後一次拉取時間、儲存量。
relink 解除連結並重新開始。必須有人掃描新的 QR 碼。

每個閱讀工具在存檔副本超過十分鐘時,會先從 WhatsApp 更新。

命名聊天

WhatsApp 只會把已知的名稱傳給此伺服器,其餘則以電話號碼顯示。為避免伺服器登入聯絡人服務(需雲端專案、同意畫面,Google 亦需申請審核),此伺服器將此工作交給你的代理,代理通常已具備取得聯絡人資料的方式。

說 "find names for my unnamed chats"。代理會呼叫 unnamed_chats,將號碼與你的聯絡人比對,並以 set_contact_names 儲存結果。它不會找到所有人:大多數未命名聊天只是一則陌生人訊息。

代理會自行被引導至此。伺服器說明中會提及,且每次「未找到聊天」與「無匹配」結果也會顯示,因為那是代理卡在名稱的時刻。link_status 也會說明有多少聊天未命名。


對代理與調整此程式的人

以下內容針對修改程式碼的人,包括在分支上工作的 AI 代理。檔案頂部針對擁有部署的人。

結構概覽

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

一個 Durable Object 包含所有資料。其 SQLite 同時為 Baileys 認證儲存與訊息儲存。Worker 除了使用 OAuth 提供者的 KV 名稱空間儲存 token 以及短暫的「pending sign-in」紀錄外,其他皆為無狀態。

檔案

檔案 工作
src/index.ts 路由。OAuth 提供者連線、登入、配對端點、/debug、cron。
src/auth.ts 誰算是擁有者。有兩種模式:密碼或登入服務。亦包括擁有者的瀏覽器會話。
src/phone.ts 任意格式的電話號碼轉為 WhatsApp JID。
src/whatsapp-do.ts Durable Object。執行 Baileys 會話,並為 Worker 暴露 RPC 方法。
src/store.ts Schema,將 Baileys 事件寫入 SQLite,及所有查詢。
src/auth-state.ts Baileys 認證狀態由 DO 的 SQLite 支援(取代 useMultiFileAuthState)。
src/mcp.ts MCP 工具,以及指引代理使用命名工具的提示。
src/pages.ts, src/landing.ts, src/privacy.ts, src/tos.ts, src/ui.ts 全部為 HTML。ui.ts 擁有共用設計 token。
src/ws-shim.ts, src/lru-shim.ts, src/polyfills.ts 讓 Baileys 在 workerd 上執行。見下方。
scripts/vendor-bridge.mjs 從 Baileys 的 Rust bridge 提取 wasm。在 npm install 上執行。
local-export/ 一個獨立的一次性指令碼,將你的歷史匯出為 JSON 檔案,無需伺服器。非部署的一部分。

會話運作方式

所有 WhatsApp 流量皆在 runSession() 的 whatsapp-do.ts 中發生。分為兩種型別:

  • 配對 (startPairing)。尚無憑證。Baileys 發出 QR 字串,保留在記憶體中並由 pairStatus() 轉譯為 SVG。掃描後,WhatsApp 會以程式碼 515(需要重新啟動)關閉 Socket,而連線會使用新憑證重新連線。歷史記錄以 messaging-history.set 區塊送達,並帶有 progress 百分比。當 WhatsApp 回報 100% 且串流已安靜幾秒鐘時,工作階段即結束。
  • 拉取 (pull)。使用儲存的憑證重新連線。WhatsApp 會交付我們離開期間佇列中的所有內容,然後發出 receivedPendingNotifications 訊號。工作階段在安靜幾秒鐘後結束。

呼叫者共用一個工作階段:在一個執行中的 pull() 期間發起第二個會取得相同的 Promise。

配對工作階段的壽命長於啟動它的 HTTP 請求。startPairing 設定 Durable Object 鬧鐘,而 alarm() 則等待工作階段 Promise。執行中的鬧鐘處理常式是保持物件存活的原因。配對頁面僅供觀看;關閉它不會停止匯入。

閒置偵測僅計算資料事件(messaging-history.set、messages.upsert),而不計算連線雜訊。計算所有內容會讓工作階段永遠保持開啟,因為上線狀態與聯絡人更新永遠不會停止。

資料模型

全部位於 Durable Object 的 SQLite 中(store.ts、initSchema)。

  • messages(chat, id, ts, from_me, sender, push_name, type, body),主鍵 (chat, id)。媒體是 body 中的標籤。編輯會更新 body;刪除會將其設為 [deleted]。
  • messages_fts,一個透過觸發條件保持同步的 body FTS5 索引。
  • chats(jid, name, is_group)、contacts(jid, name, notify) 和 custom_names(jid, name)。顯示名稱的解析順序為:WhatsApp 的聊天室名稱、WhatsApp 的聯絡人名稱、儲存的名稱、該人員自己的推送名稱,然後是號碼。custom_names 是 set_contact_names 寫入的內容。
  • alias(lid, pn)。WhatsApp 正在轉向匿名 @lid 識別碼。這會將它們對應到電話號碼 JID,而 addAlias 會將現有資料列摺疊成電話號碼形式,以便一個人就是一個聊天室。
  • auth(k, v),Baileys 憑證與訊號金鑰,由 Baileys 的 BufferJSON 編碼為帶有 Buffer 的 JSON。
  • meta(k, v),目前僅有 last_pull。

資料匯入具冪等性。重新匯入歷史記錄會執行 upsert:它會填入遺失的 sender 或 push_name 且絕不會重複。這就是重新連結安全的原因。

修改前需知

這些全部需要花費實際時間來尋找。在碰觸 Baileys 相關程式碼之前請先閱讀它們。

  1. Baileys needs three shims to run on workerd.
    • ws:該套件需要 Node socket。ws-shim.ts 包裝了 Workers WebSocket,透過 fetch() 和 Upgrade 標頭開啟,因為這是設定 WhatsApp 檢查的 Origin 標頭的唯一方法。
    • whatsapp-rust-bridge:它以 base64 形式發布其 wasm 並在執行階段編譯,這是 workerd 所禁止的。vendor-bridge.mjs 將 wasm 寫入檔案並修補載入器,將其匯入為 CompiledWasm 模組。vendor/ 資料夾是自動產生的,未提交至版控。
    • 計時器:Node 計時器具有 .unref(),而 workerd 則無。polyfills.ts 新增了它。它必須是 Worker 和 Durable Object 模組中的第一個匯入。
  2. lru-cache 自動清除會破壞第一次匯入。 Baileys 使用 ttlAutopurge: true 建立快取,這會為每個專案啟動一個 setTimeout。workerd 允許每個物件有 10,000 個待處理計時器,而第一次歷史記錄同步會以 QuotaExceededError 突破該限制。lru-shim.ts 會關閉自動清除。過期專案仍會在存取時以延遲方式捨棄。
  3. QR 配對永遠不會設定 creds.registered。 該旗標屬於行動裝置註冊流程。「是否已連結」是 creds.me?.id。
  4. 歷史記錄每個連結只會來一次。 如果第一次匯入中途失敗,該歷史記錄就會遺失,直到您重新連結為止。在未針對真實的第一次連結進行測試之前,切勿發布對匯入程式碼的更改。
  5. 群組傳送者會出現在兩個地方。 即時訊息具有 key.participant。歷史記錄訊息將其放在 msg.participant 上。兩者都要讀取。
  6. 精確釘選 Baileys 版本。 這是一個預先發布版本,其 API 會在候選版本(RC)之間變動。
  7. 部署不會驅逐正在忙碌的 Durable Object。 具有活躍會話的物件會繼續執行舊程式碼,直到會話結束。使用遷移重新命名類別也未驅逐它。若你已經發布修正程式但物件仍表現異常,請停止向其傳送請求,並等待三到五分鐘。每一次探測,包括 /debug,都會重置其閒置計時器,因此輪詢已過時的物件會使其保持過時。Baileys 在 socket 關閉時也會洩漏間隔計時器;finish() 在 whatsapp-do.ts 中清除它們,讓物件能夠完全進入閒置狀態。
  8. 登出需要兩個步驟。 logout() 首先連線並呼叫 Baileys 的 logout(),因此裝置會從手機列表中消失,接著清除本機憑證。若第一步失敗,會在除錯事件中顯示,使用者必須手動移除裝置。
  9. WhatsApp 在應該存名字的地方儲存佔位符。 對於沒有聯絡人的聊天,會以號碼命名,有時會被遮蔽(+972∙∙∙∙∙∙∙20),在聊天名稱和聯絡人名稱中。numberLike 在 store.ts 中將它們視為無名稱,並且 ingest 拒絕儲存。 在此修正之前,佔位符優先於真實儲存的名稱。
  10. 不要呼叫 markOnlineOnConnect。 它被故意設為 false。將已連結裝置標記為線上會使手機停止顯示通知。

設定

名稱 型別 用途
OWNER_PASSWORD secret 密碼模式(預設)。12 個以上字元,否則登入失敗。
OWNER_EMAIL var Login-service 模式:允許的唯一電子郵件。
OPERATOR_NAME var, optional 顯示於隱私頁面,說明誰在執行此伺服器。
DEFAULT_COUNTRY_CODE var, optional 國碼(數字,無 + 號),在代理程式儲存未加國碼的號碼時使用,例如 972。
LOGIN_ORIGIN var 將此設為 true 以切換至 login-service 模式。
ALLOWED_ORIGINS var Login-service 模式:以逗號分隔的來源,其 token 會被接受。生產環境應僅列出生產環境。
PIN secret, optional 32 個以上隨機字元。透過 x-pin 標頭啟用 /debug 路由。若未設定,則保持關閉。
OAUTH_KV binding OAuth 提供者的 Token 儲存與待處理登入。
WA binding Durable Object。

將部署自己的值(domain、KV id、email)儲存在 wrangler.local.jsonc,該檔案會被 git 忽略,並使用 npm run deploy:local 部署。wrangler.jsonc 是其他人起始的範本。

調整

  • 新增工具。 在 mcp.ts 中註冊,並將 SQL 放入 store.ts。讀取工具應先呼叫 fresh()。除非另有說明,否則保持工具唯讀。若失敗導致代理程式卡住,請在工具結果中說明下一步嘗試方式,像 NAME_HINT 對缺失名稱所做的那樣。
  • 變更可登入的人員。 auth.ts 選擇一種模式(authMode),index.ts 中的路由會遵循此模式。若要新增其他身份驗證提供者,請在此處新增一種模式並新增回呼路由。其餘程式碼僅能看到一個不透明的 bearer token。
  • 使其支援多使用者。 目前 Durable Object 以固定名稱 owner 進行呼叫(wa() 位於 index.ts,handleMcp 位於 mcp.ts),而 userId 是 OAuth 引數中的常數 'owner'。若要支援多使用者,請從已登入的身分衍生兩者,並以此為鍵鎖定物件。請先考慮是否需要保留他人訊息歷史。
  • 其他填寫名稱的方法。 任何能產生 { jid, name } 列的都可呼叫 store.setCustomNames。CSV 上傳、CardDAV 拉取或排程工作都可行。代理路由已存在,伺服器不需要任何憑證。
  • 傳送訊息。 未實作,純粹是設計。 在新增之前請三思。

除錯

設定 PIN 後,這些會回傳 JSON,並需要標頭 x-pin: <PIN>:

  • /debug:會話階段、是否已連結、計數,以及來自會話的最新事件(連線變更、歷史區塊大小、錯誤)。
  • /debug/pull:立即執行拉取。
  • /debug/chats?q=, /debug/messages?chat=, /debug/search?q=, /debug/types: 讀取儲存庫而不經過 MCP.
  • /debug/unnamed: 這些聊天 unnamed_chats 會被回傳。
  • /debug/sql?q=: 對儲存庫進行一次唯讀 SELECT,以找出名稱或計數看起來錯誤的原因。
  • /debug/logout: 與 relink 相同。

本機開發:npm run dev 搭配一個包含 OWNER_PASSWORD=...(12+ 個字元)的 .dev.vars。它執行一個本機 Durable Object,並有自己的空儲存,讓你可以練習登入流程並看到真實 QR 碼,但從本機開發環境連線手機會產生第二個已連結裝置。

若要在沒有手機的情況下測試配對 UI 狀態,請以模擬的 /pair/status 服務此頁面。頁面所需的一切只是一個 JSON 形狀:paired、running、progress、qrSvg、messages、chats。

檢查變更

  1. npm run typecheck,然後 npm test。測試會將真實儲存庫程式碼對照 Node 內建 SQLite(Node 22.13 或更新版本)執行,包括從舊版遷移。該 SQLite 沒有 FTS5,故搜尋本身未被覆蓋。
  2. 部署後,使用 PIN 執行 GET /debug。它應該回傳 phase: "idle" 與 paired: true。
  3. GET /debug/pull。一個安靜的帳號應該在大約十秒內回傳 reason: "idle"。
  4. 呼叫 list_chats,然後在希伯來語或表情符號較多的聊天上執行 get_messages,接著執行 search_messages。再執行 unnamed_chats,並在一個號碼上執行 set_contact_names,並確認聊天顯示名稱。
  5. 如果你觸碰了 ingest,重新連結並確認已儲存的訊息數量不會增長(重新匯入是冪等的),以及群組訊息顯示真實寄件人名稱。

已知缺口

  • WhatsApp 中沒有名稱且未儲存的聊天會顯示為電話號碼。名稱不會自行重新整理:只有代理程式(或你)儲存時才會儲存。
  • 若每日拉取發現會話已登出,沒有任何提示。link_status 在你的代理程式下次詢問時會顯示它。
  • 編輯與刪除在以協議訊息到達時套用。此路徑尚未對真實訊息進行測試。
  • 僅對一個真實帳號進行過測試。

授權

MIT