Originally published at webofmike.com on 2026-10-20. The demo repo and every command in it were run before publishing.
A Show HN post for a "WhatsApp MCP server running free on Cloudflare" hit 124 points and 69 comments in under 16 hours on October 19, and none of the 69 comments answered the one question that mattered: what does this thing actually send. The thread argued instead about whether the free tier is actually free (a commenter quoted the README saying scale needs a paid Cloudflare plan), whether running it risks a WhatsApp ban (several people said yes, from experience), and whether decrypting end-to-end-encrypted messages and uploading them to an LLM is a privacy problem (it is, for everyone in the conversation who didn't agree to it).
I built mcp-audit-tap to answer that question for any stdio MCP server, not just this one. It's about 100 lines of Python, no dependencies, and it produces a log instead of an opinion.
The thread that couldn't see its own traffic
The same day, a separate HN thread asking "if you don't run your own LLM proxy, how do you know what's going on" pulled 213 points and 154 comments. The top reply argued against running a proxy at all, in favor of OpenTelemetry traces from the coding agent itself. Both threads are circling the same gap from opposite sides: one is a server you didn't write asking for access to something personal, the other is a client you didn't fully instrument asking you to trust it. In both cases the honest answer to "how do you know what's going on" was: you don't, unless something is watching the wire.
MCP makes this easy to skip. A server's README describes what it's supposed to do. Its source, if you read it, describes what it's capable of doing. Neither tells you what it actually sent in the request that just ran, with the arguments your agent actually passed and the response your agent actually got. That's a wiretap's job, and MCP's stdio transport, a server and client exchanging line-delimited JSON-RPC over stdin and stdout, makes building one almost too simple.
What the tap does
audit_tap.py spawns the real MCP server as a child process. Your client talks to the tap instead of the server; the tap relays every line between them unchanged, and in parallel inspects it:
MCP client <-- stdio --> audit_tap.py <-- stdio --> the real MCP server
|
v
audit.log (JSONL)
A tools/call request gets logged with the tool name and arguments. The matching result, found by JSON-RPC id, gets scanned against a small set of regex patterns and logged with whatever matched. Running the included toy server, which simulates a personal-messaging tool the way the WhatsApp MCP server would look from the outside, through the tap with no flags produces this:
{"ts": "...", "direction": "client->server", "event": "tools/call request", "tool": "read_latest_messages", "arguments": {}}
{"ts": "...", "direction": "server->client", "event": "tools/call result", "tool": "read_latest_messages", "flags": ["phone", "email"], "preview": "Jordan: Running late, call me at 555-867-5309 if plans change..."}
The client gets the exact same response it would have gotten without the tap. Nothing is altered. What changed is that there's now a line in a file saying a phone number and an email address just left the server, which tool returned them, and when.
Observe first, then decide whether to act
Pass --redact and the same call produces a third log line and a different response:
{"ts": "...", "direction": "tap", "event": "redacted before forwarding", "tool": "read_latest_messages", "flags": ["phone", "email"]}
The text the client receives now reads call me at [REDACTED:phone] instead of the real number. That's the entire distance between an audit tool and a policy enforcement point: one if self.do_redact branch in a sixty-line class. I didn't write that branch first. I wrote the logging first, ran it, and only added redaction once the log told me there was something worth redacting.
That ordering matters more than the code. The instinct when you hear "a tool is leaking PII" is to reach for a blocklist. But a blocklist written before you've looked at real traffic is a guess, and MCP tool results are varied enough that guesses miss things. PII_PATTERNS in audit_tap.py is a plain dict of name to compiled regex. I shipped it with two patterns, phone numbers and email addresses, because that's what the demo's fixture data contains. The honest way to extend it is to run the tap against your actual server first, read the log, and add patterns for whatever shows up that you didn't expect.
Running it against a real server
The demo ships a toy server so the README is self-contained, but the tap doesn't know or care what it's wrapping. If your MCP client config currently launches a server directly:
{"command": "python3", "args": ["-m", "some_mcp_server"]}
point it at the tap instead:
{"command": "python3", "args": ["audit_tap.py", "--log", "audit.log", "--", "python3", "-m", "some_mcp_server"]}
Every message still flows through in both directions. The only new thing is audit.log filling up with a record of what that server actually does when your agent calls it, which you can read before you decide the server is worth the account-ban risk the HN commenters were debating, or worth building a real policy around.
Quickstart
No dependencies beyond Python 3.9+.
git clone https://github.com/themsquared/mcp-audit-tap.git
cd mcp-audit-tap
python3 test_harness.py -- python3 demo_server.py
That runs the toy server directly, with nothing watching, so you can see the baseline response. Then run the same call through the tap:
python3 test_harness.py -- python3 audit_tap.py --log audit.log -- python3 demo_server.py
cat audit.log
And with redaction on:
python3 test_harness.py -- python3 audit_tap.py --redact --log audit.log -- python3 demo_server.py
test_harness.py is a minimal MCP client I wrote so the whole thing runs without needing Claude Desktop or any other real agent host installed. It sends initialize, tools/list, and one tools/call, which is enough to see the tap do its job end to end in under a minute.
What a regex won't catch
The two patterns in the demo, a US-shaped phone number and an email address, are intentionally the easy case. A regex matches a shape, not a meaning. It won't flag a message that says "my mother's maiden name is Alvarez" or a block of base64 that decodes to a photo. It also won't survive adversarial formatting: 5558675309 with no separators slips past a pattern written for 555-867-5309, and a determined server could split a number across two content blocks in the same result just to dodge a naive scanner.
None of that is a reason to skip the tap. It's a reason to read audit.log instead of trusting a clean report. The log keeps the full preview of what passed through, flagged or not, so a human or another process can check for what a hand-written pattern misses. A cleared flag does not mean nothing slipped through; it means nothing matched the patterns written so far. What the tap guarantees is a record of what crossed the wire, which is the one thing neither HN thread had on October 19.
Where this stops
A tap is not a gateway. It has no authentication, no rate limits, and nothing resembling a policy language, on purpose. agentgateway's MCP guardrails run inside an actual gateway, see the resolved tools/call request and result at the protocol layer, and can reject a call outright before the tool ever executes, using CEL expressions instead of a Python dict of regexes. For traffic-layer pattern blocking with a real rule engine, WAF for LLM and MCP traffic covers putting Coraza and OWASP CRS in front of prompts and tool arguments.
None of that is where you start. You start by finding out what a server you didn't write is actually sending, which is the one thing a 69-comment HN thread about trusting a free WhatsApp integration never managed to do. The code is at github.com/themsquared/mcp-audit-tap.
Frequently asked questions
What is an MCP audit tap?
A process that sits between an MCP client and server on stdio, relays every JSON-RPC message unchanged, and in parallel writes a structured log of each tools/call request and result. It observes by default; it does not block or alter traffic unless you explicitly turn on redaction. The point is visibility before any policy decision.
How do you see what a third-party MCP server actually sends without reading its source?
Run it behind a tap instead of running it directly. mcp-audit-tap spawns the real server as a child process and relays stdin/stdout between it and your client, logging the content of every tool call and result to a JSONL file as it passes through, including flags for patterns like phone numbers and email addresses.
Is it safe to run a random MCP server, like the WhatsApp one from Show HN, that connects to a personal account?
The HN thread for it raised real points: several commenters reported account bans from similar WhatsApp bridges, and the tool decrypts end-to-end-encrypted content before uploading it to an LLM. Neither risk goes away by adding an audit tap. What a tap adds is a record of exactly what left your machine, so you are not relying on the README's word for it.
What's the difference between an MCP audit tap and an MCP gateway?
An audit tap observes and optionally redacts a single server's traffic, with no authentication, routing, or policy language. An MCP gateway like agentgateway sits in front of many servers, authenticates callers, and can reject a tools/call outright based on CEL rules before the tool runs. A tap is how you find out what a server actually sends before deciding whether you need a gateway's enforcement on top of it.
Canonical version, with machine-readable markdown at https://webofmike.com/mcp-audit-tap/index.md: https://webofmike.com/mcp-audit-tap/
Top comments (0)