Skip to content
Back to projects

Open Source - MCP Server

WhatsApp MCP Server

GoPythonMCPFastMCPwhatsmeowSQLite (FTS5)DockernginxPrometheusHetzner

An MCP server that lets an agent search and act on my own WhatsApp history without shipping that history to a hosted service. It runs as two Docker containers on a small Hetzner box behind nginx, and the only credential is a bearer token I generate. Open source under MIT at github.com/HalemoGPA/whatsapp-mcp-server.

The part worth reading is the tool budget. MCP injects every tool definition into the model's context on every single request, used or not, so 96 tools is a fixed tax of roughly 20k tokens before the model does anything at all. The token bill was the boring half of the problem. The real one was selection: with all 96 in front of it, the model picked the right tool less often on my task set. So the default toolset serves 29 hot-core tools directly and prunes the other 67 from the served set, but captures the full library first so nothing becomes unreachable. Two meta-tools bridge the gap: find_tool(query) ranks the whole 96-tool library and returns name, description, and parameter signature, and call_tool(name, arguments) dispatches to any of them. Always-on cost drops from about 20k tokens to about 8k with no capability removed and no mode to flip at call time.

The security consequence is the interesting part. call_tool dispatches inside the process, past the MCP on_call_tool middleware chain, so it has to re-apply everything that chain would have done: per-tool scope enforcement and audit logging of mutating calls. Both are mirrored in toolsearch.py, because a retrieval layer that quietly skips your authorization middleware is worse than no retrieval layer at all.

The retrieval is measured rather than asserted. tests/toolsearch-eval/ holds 40 hand-written cases plus 191 more generated by a separate agent that only saw the tool catalog and phrased tasks in deliberately slangy language, independent of the alias map, because an eval tuned against its own answer key measures nothing. Lexical recall@8 caps around 75% on that adversarial set, and that number is published in the repo because a retrieval layer without a number attached is a vibe. An end-to-end routing probe then settles the question that actually matters: two identical agents routed the same 22 hard-weighted cases, one with all 96 tools loaded and one with the core plus find_tool, and both scored 21/22 with identical decisions and the same single ambiguous miss.

Three other problems shaped the codebase. WhatsApp hides phone numbers behind LIDs inside groups, so one person appears under their number in DMs and under an opaque LID in group history, and any tool filtering on a single sender silently returns half their messages with no error. identity.py reads whatsmeow's own mapping tables read-only and joins the two into one identity; it is additive, never writes, and anything unmapped resolves to itself, so it can make attribution more accurate but never confidently wrong. Voice notes are made searchable by a background worker that decrypts the audio, transcribes it through a hosted API, and writes to a separate database, because the message store is mounted read-only and the bridge rewrites it with INSERT OR REPLACE, so a transcript column would be silently wiped on any resync. Arabic gets its own normalisation pass, since clitics attach as prefixes and plain token matching never matches فارماسي against a stored الفارماسي.

On performance, measured against the upstream project on the same 77k-message store: SQLite indexes turned full-table scans of 200ms+ into single-digit-millisecond seeks, an FTS5 contentless mirror made content search 12x to 3000x faster depending on selectivity, history sync batched into transactions ingests 10x to 50x faster, and the images went from 201MB to 55MB for the bridge and 1.05GB to 509MB for the server.

The repo also carries an editorial position. docs/protocol-vs-app.md catalogues where WhatsApp's wire protocol and its app UI disagree, marking every claim as verified, sourced, or speculative. Several capabilities in there are deliberately not implemented: the wire permits them, but they work by deceiving a recipient rather than by reading data already delivered to me. Reading my own message history is unremarkable; forging a quoted reply is a different kind of thing. The finding is documented so the boundary is on the record; the recipe is not.

It started as a fork of lharries/whatsapp-mcp, which supplied the whatsmeow bridge skeleton and the first read tools. Of the 14 files in the upstream tree one is carried unchanged; the Go bridge grew about 5x and the Python server about 4x, and the tool retrieval, transcription, media recovery, scheduling, observability, identity, embeddings, deploy, and test trees are all new. 95 offline tests run in CI on every push.