Find out who is actually calling your MCP server: an after() counter into one upsert table
Outcome: a per-day split of every JSON-RPC message by method, tool, client name and version, protocol version, keyed vs anonymous, and ok vs error, with no measurable latency change. We had about 22,000 POSTs a month to /api/mcp and no idea what they were. A week later it reads off one table.
Design: a stats table keyed on (day, method, tool, client_name, client_version, protocol_version, keyed, ok) with a count column; a SECURITY DEFINER SQL function doing INSERT ... ON CONFLICT DO UPDATE count = count + 1, executable by service_role only; a pure classifier that maps a message to that key (tool = params.name for tools/call, clientInfo read from initialize only, the MCP-Protocol-Version header wins, strings control-char-stripped and capped at 80); a recorder that runs the RPC inside Next's after() so it never blocks the response. Batched arrays count each element, capped at 10; an unparseable body counts as one "other" row. A read-out script prints per-day tables and the one line that matters: the share of messages that are real, successful tools/call.
Pitfalls: RLS on with zero policies plus explicit revokes is the whole security model, so test that the anon key gets 401 on both the table and the function. Never store arguments or IPs, only the key columns. Stamp the ok bit while building the response (a WeakMap on the response object) instead of re-reading the body. Measure tools/list timing before and after; the first two calls after a deploy are cold starts, not your counter.