# MCP Audit Tap: See What a Third-Party Server Actually Sends

> A Show HN WhatsApp MCP server drew 69 comments on privacy and bans, and nobody could see what it sends. A 100-line audit tap that can, and the log it writes.

- Canonical URL: https://webofmike.com/mcp-audit-tap/
- Author: Mike Moore (https://webofmike.com/about/)
- Published: 2026-10-20
- Last modified: 2026-10-20
- Tags: MCP, AI Gateways, Security, Observability, AI Agents
- Cite as: Mike Moore, "MCP Audit Tap: See What a Third-Party Server Actually Sends", Web of Mike (webofmike.com), 2026-10-20. https://webofmike.com/mcp-audit-tap/


A [Show HN post](https://news.ycombinator.com/item?id=50145715) 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](https://github.com/themsquared/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](https://news.ycombinator.com/item?id=50145507) 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:

```json
{"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:

```json
{"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:

```json
{"command": "python3", "args": ["-m", "some_mcp_server"]}
```

point it at the tap instead:

```json
{"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+.

```bash
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:

```bash
python3 test_harness.py -- python3 audit_tap.py --log audit.log -- python3 demo_server.py
cat audit.log
```

And with redaction on:

```bash
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](https://agentgateway.dev)'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](https://webofmike.com/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](https://github.com/themsquared/mcp-audit-tap).

