WhatsApp MCP Assistant

WhatsApp MCP Assistant

An MCP server that provides hybrid-retrieval RAG search, ML analysis (sentiment, topic clustering, stats), and WhatsApp Cloud API message sending over WhatsApp chat history.

Category
访问服务器

README

CI <!-- Replace YOUR_USERNAME/YOUR_REPO once pushed, or the badge will be broken. -->

WhatsApp MCP Assistant: RAG search + ML analysis + message sending

An MCP server over WhatsApp chat history combining all three things discussed: hybrid-retrieval RAG search over the conversation, ML analysis (sentiment, topic clustering, real stats), and WhatsApp Cloud API message sending -- exposed as five real MCP tools.

Builds directly on the design proven in the docs-RAG project (hybrid BM25 + TF-IDF/LSA retrieval, real MCP protocol testing), retargeted at conversational data, which has its own genuinely different challenges from documentation (short messages, informal language, multi-speaker attribution, timestamps).

A real bug found live: iPhone export format wasn't supported

While actually testing this against a real, live-exported chat (not the synthetic sample data), the pipeline crashed with a cryptic ZeroDivisionError three layers deep inside the BM25 library. Root cause, confirmed directly: iPhone WhatsApp exports use a completely different format than Android:

Android:  12/01/24, 09:15 - Priya: message text
iPhone:   [12/01/24, 9:15:32 AM] Priya: message text

Bracketed instead of dash-separated, includes seconds, uses 12-hour AM/PM instead of 24-hour time -- the parser only handled the Android format, so every line in a real iPhone export silently failed to match, producing zero parsed messages, which then crashed deep inside a third-party library instead of failing with a clear message at the actual source of the problem.

Fixed properly, not patched around: the parser now tries both formats, correctly converts 12-hour AM/PM to 24-hour time, and handles a real, easy-to-miss quirk -- iPhone exports commonly use U+202F (a narrow no-break space, invisible when eyeballing the file) instead of a plain space before "AM"/"PM", which a plain-ASCII regex silently fails to match. Also added: ChatPipeline now raises a clear, specific error ("Parsed zero messages... check that this is really a WhatsApp export file") if a chat file produces zero messages, instead of letting that surface as an unrelated crash inside a dependency three call-stack frames away from the actual cause.

Attempted improvement that honestly didn't help: a trained reranker

The docs-RAG project's biggest quality lever was a trained reranker on top of hybrid retrieval. It was missing here, so it was built (wa_mcp/reranker.py, same design: logistic regression trained from scratch on BM25/dense scores, term overlap, chunk length, plus a chat-specific "distinct senders in this chunk" feature) and rigorously measured, not just added and assumed to help.

Real result, on the held-out test queries:

Hybrid alone:       precision@5=0.200  recall@5=1.000  mrr=0.815
Hybrid + reranker:  precision@5=0.200  recall@5=1.000  mrr=0.726

The reranker measurably does NOT help here, and slightly regresses MRR. Most likely cause: only 8 training queries (120 examples, 8 positive) is too small a training set for the reranker to learn reliable weights -- smaller than the docs project's training set, on a task where hybrid retrieval alone is already close to its ceiling at this corpus size.

Decision: not wired in as the default, specifically because shipping something that measures worse while calling it an "improvement" would be dishonest. The code is real, tested for correctness (tests/test_reranker.py -- training produces valid, imbalanced labels; predictions are valid probabilities), and available (wa_mcp/reranker.py) for a much larger real chat where a bigger, real-usage-derived training set might behave differently -- but it's not claimed as a proven improvement, because it isn't one, measured honestly.

The improvement that DID measurably help was the filler-message filtering described above -- that one is wired in as the default, because it was actually measured to help.

Attempted improvement #2: a trained filler classifier -- helped on its own test, then failed at real scale

The fixed FILLER_PHRASES list has an honest, stated limitation: it can only catch filler wording someone thought to write down. So a trained alternative was built (wa_mcp/filler_classifier.py, logistic regression on TF-IDF, same pattern as the sentiment classifier) and compared directly against the fixed list -- including on filler phrasings deliberately absent from the fixed list ("hbu", "no cap", "word", "true story", "for real").

On its own held-out test set, the trained classifier looked like a clear win:

accuracy: 0.9   precision: 1.0   recall: 0.8

It correctly caught 4 of 5 novel slang variants the fixed list would have completely missed, while never wrongly flagging a real, substantive message.

Then it was tested against the actual 60,000-message stress-test corpus used earlier -- and it did measurably WORSE than the fixed list:

Fixed list:          2 chunks remaining after filtering
Trained classifier:  1,728 chunks remaining after filtering

The trained classifier let obvious filler through repeatedly ("call me later", "wait what" appearing in its own top search results, unfiltered).

Root cause, confirmed directly, not guessed: checked exactly which filler phrases the 60k-message test corpus actually used against what the classifier's 28-example training set had ever seen -- 14 of the 22 real filler phrases in that corpus were never in the classifier's training data at all ("yeah", "wait what", "sounds good", "call me later", "checking now", and 9 others). The classifier isn't learning a general concept of "this message carries no retrieval-worthy content" -- with only 28 examples, it's memorizing its own narrow training vocabulary, the same fundamental limitation as the fixed list, just less predictable and harder to inspect/edit than a plain list you can read and extend directly.

This is a genuinely important, humbling lesson, not a wasted attempt: a small held-out test set drawn from a similar, narrow vocabulary can make a tiny trained model look like it generalizes well, right up until it meets real data with a genuinely different vocabulary distribution -- exactly the gap between "measures well on its own test set" and "actually works in the real world" that's worth knowing about as a general ML lesson, not just for this one feature.

Decision: the fixed list remains the default (use_trained_classifier=False in chunk_messages()), specifically because it measurably performed better on the real, at-scale test that actually matters. The trained classifier is kept available (use_trained_classifier=True) and tested for correctness (tests/test_filler_classifier.py) -- but not claimed as an improvement, because real testing found the opposite. A genuinely larger training set (hundreds of real labeled examples, not 28) would likely change this finding; not built here, since that's real, substantial labeling effort, not a quick fix.

A real scaling investigation: tested at 60,000 messages, not assumed

This project was originally built and tested against a 66-message synthetic sample. A real question came up during use: does this actually work at real chat scale (tens of thousands of messages)? Rather than guess, a realistic 60,000-message synthetic chat was generated (mostly short filler messages -- "lol", "ok", "one sec" -- with one real, findable conversation planted in the middle) and tested directly.

First result: it does NOT crash and is NOT slow -- built in 5.9s, searched in 0.035s even at 60k messages. But retrieval QUALITY genuinely degraded: the planted conversation about booking "Cotton Villa" got diluted across noisy chunks full of filler messages, and none of the top-5 results was a clean match.

Root cause: with ~20,000 sliding-window chunks at this scale, a 5-message chunk that's 80% filler ("yep", "one sec", "wait what") has far less distinguishing content for BM25/TF-IDF to match against than one where every message carries real content -- filler doesn't just fail to help, it actively dilutes genuine signal sitting in the same window.

Fix: wa_mcp/chunking.py now filters out common filler messages before chunking (they're still counted in message-count stats, just excluded from retrieval chunks specifically). Re-tested against the exact same 60k-message file: chunk count dropped from 19,999 to 2, and both top results were now clean, correct matches for the planted conversation.

Honest limitation, not glossed over: the filler list is a fixed set of phrases, not a trained classifier -- it will always miss some real-world phrasings. One was caught and fixed during this very investigation ("haha true" wasn't in the initial list, only found by testing against the large synthetic file and inspecting actual results). A more robust version would train a small classifier the same way the sentiment model was trained, rather than hardcode a list -- not implemented here, stated as a real next step rather than presented as solved.

Also worth noting honestly: the synthetic test chat was closer to a worst-case (nearly all filler except one planted signal) than a typical real chat, which usually has a higher proportion of substantive messages. The mechanism (filtering filler improves signal-to-noise at scale) is validated; the exact magnitude of improvement on any specific real chat will vary.

Data: synthetic, not anyone's real private chat

data/sample_chat.txt is a hand-written, fictional conversation (three people planning and taking a trip), written in the REAL WhatsApp export text format (DD/MM/YY, HH:MM - Name: message). The parser handles this real format, so a genuine exported chat (Settings > Export Chat in WhatsApp) would work too -- deliberately not used here, same reasoning as writing a fictional docs corpus instead of scraping real documents.

Part 1: RAG search over chat history

Individual WhatsApp messages are often 1-5 words ("yes", "lol", "on my way") -- too short to retrieve meaningfully alone. wa_mcp/chunking.py groups messages into overlapping sliding-window chunks (window=5, stride=3) so retrieval has something substantive to match against, with the overlap ensuring a conversational moment near a window boundary is still captured whole in at least one chunk.

Retrieval reuses the proven hybrid design (BM25 + TF-IDF/LSA + Reciprocal Rank Fusion, with Porter stemming) from the docs-RAG project.

Honest note on retrieval quality on chat data specifically: casual chat text is measurably harder to retrieve well than clean documentation prose. A real example found during testing: the query "who was stressed about work" found the correct chunk at rank 4 out of 22, not rank 1 -- reasonable (top 20%), but not perfect, and reported as such rather than only testing easy queries that happen to rank first.

Part 2: ML analysis

  • Sentiment classifier (wa_mcp/sentiment.py): logistic regression TRAINED FROM SCRATCH on hand-labeled, informal chat-style examples (not clean prose) -- same "no pretrained model" reasoning as the docs project's reranker (this sandbox has no access to huggingface.co). Real, honestly reported result on a held-out test set: 70% accuracy, 62.5% precision, 100% recall. Clearly better than random guessing on a small, hand-labeled dataset -- and clearly limited by that small dataset size, not presented as more than it is.
  • Topic clustering (wa_mcp/topics.py): TF-IDF + KMeans, classical, no download needed. Tested against a real, checkable claim: every chunk mentioning "Cotton Villa" (the accommodation) lands in the same cluster, distinct from the beach-day and stress-venting conversations.
  • Chat statistics (wa_mcp/stats.py): message counts per sender, average response time between different senders, busiest day -- all directly computed, including honestly handling a real tie (two participants are tied at 23 messages each in the sample data; tested as "one of the tied leaders," not asserting a single winner as if the tie didn't exist).

Part 3: WhatsApp message sending -- the honest limitation

wa_mcp/whatsapp_client.py is written to Meta's actual, currently- documented Cloud API request format (POST /{phone_number_id}/messages, Bearer token auth, messaging_product/to/type/text JSON body -- verified via Meta's developer docs, not assumed from memory).

This has never been verified against the live API. This sandbox has no network access to graph.facebook.com (confirmed directly: a plain curl request to it returns 403, same category of block as huggingface.co earlier), and there's no real WhatsApp Business Account or access token available regardless. Tests (tests/test_whatsapp_client.py) mock the HTTP layer to verify the client builds requests correctly against Meta's documented contract -- that is NOT the same claim as "this works against the real API," and this README says so explicitly rather than blurring the two.

One genuinely interesting thing found while building this: calling send_whatsapp_message with a placeholder token, without any mocking, actually attempted a REAL network request and came back with HTTP 403 -- confirming this sandbox's network block directly, and confirming the client fails cleanly (returns a proper error result) rather than crashing, even against a real, if blocked, network attempt -- not just against mocked failures.

Build & run

python3 -m venv venv
./venv/bin/pip install -r requirements.txt

Run tests:

./venv/bin/python -m pytest tests/ -v

Try it live (uses the fictional sample chat by default):

./venv/bin/python -m wa_mcp.interactive_demo

Or point it at a real chat you've exported yourself (Settings > Export Chat in WhatsApp, no API needed -- both Android and iPhone export formats are supported):

./venv/bin/python -m wa_mcp.interactive_demo ~/Downloads/_chat.txt

Run the MCP server:

./venv/bin/python -m wa_mcp.server

To actually send real WhatsApp messages (untested against the live API, see above), set real credentials first:

export WHATSAPP_PHONE_NUMBER_ID="your-real-id"
export WHATSAPP_ACCESS_TOKEN="your-real-token"
./venv/bin/python -m wa_mcp.server

Test suite

tests/test_parser.py            10/10 -- both Android + iPhone export formats,
                                          incl. multi-line messages and the
                                          narrow-no-break-space quirk
tests/test_chunking.py           6/6  -- sliding window + overlap + filler filtering
tests/test_pipeline.py           2/2  -- clear error on unrecognized format
tests/test_retrieval.py          3/3  -- hybrid search on real chat queries
tests/test_sentiment.py          5/5  -- trained-from-scratch classifier, real train/test split
tests/test_stats_and_topics.py   5/5  -- real computed stats + topic separation
tests/test_whatsapp_client.py    3/3  -- mocked HTTP layer, documented contract only
tests/test_reranker.py           3/3  -- reranker mechanics (see honest finding above:
                                          not wired in as default, doesn't measurably help)
tests/test_filler_classifier.py  4/4  -- trained classifier mechanics (see honest
                                          finding above: fixed list wins at real scale)
tests/test_mcp_server.py         6/6  -- real MCP protocol, all 5 tools

47/47 passing.

Known limitations (stated plainly)

  • WhatsApp sending is untested against the live API -- the single biggest, most important honest caveat in this project. See Part 3.
  • Small, synthetic chat data (66 messages). Real stats/retrieval numbers, but from a small sample -- a genuine multi-thousand-message export would behave somewhat differently at scale.
  • Small hand-labeled sentiment dataset (28 train / 10 test examples) -- 70% accuracy reflects that data size honestly, not a ceiling on the approach itself.
  • No template message support -- Meta requires pre-approved templates for messages sent outside a 24-hour customer service window; only free-form messages are implemented here.

Layout

data/sample_chat.txt      hand-written, real-format synthetic chat export
eval/sentiment_data.py    hand-labeled TRAIN/TEST sentiment examples
wa_mcp/
  parser.py               real WhatsApp export format parser
  chunking.py             sliding-window conversational chunking
  retrieval.py            hybrid BM25 + TF-IDF/LSA + RRF
  sentiment.py            logistic regression, trained from scratch
  topics.py               TF-IDF + KMeans clustering
  stats.py                message counts, response times, busiest day
  whatsapp_client.py      Cloud API client (mocked-tested, see Part 3)
  pipeline.py             ties it all together
  server.py               the actual MCP server (FastMCP)
  interactive_demo.py     "watch it work" CLI
tests/                    one file per layer, incl. real MCP protocol tests

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选