Does your MCP server say what comes back?
Every tool in the registry declares what to send it. Fewer than one in nine declares what you get back.
On 29 July 2026 I walked the official MCP registry — 60,892 entries, 609 pages — took every remote
endpoint, and asked the 5,722 that completed an initialize handshake for their tool list.
5,014 answered with one I could parse.
| Across 13,715 mutating tools | count | share |
|---|---|---|
declare inputSchema | 13,715 | 100% |
declare outputSchema | 1,535 | 11.19% |
| declare no output contract at all | 12,180 | 88.81% |
A tool counts as mutating here if the server's own annotation says readOnlyHint: false
(11,639 of them) or its name begins with a mutating verb and it declares no annotation either way (2,076,
counted separately because that is weaker evidence).
Check your own, in about ten seconds
Paste your endpoint. The command runs on your machine against your server; nothing is sent here, and there is nothing for you to trust.
Why this is not a bug report
outputSchema is optional in MCP, so declaring none breaks nothing and 88.81% of the
ecosystem is in the same position. I am not telling anybody their server is broken.
The narrower thing is this. When the return shape is undeclared, the only outcome signal a caller
gets is isError. And isError: false means your server did not throw
— not that the far side committed. So for a tool that writes into something you do not own, the call
that landed and the call that quietly did not are the same artifact to whatever is
holding the other end. Not similar. Identical.
Which is why reading your own logs cannot find it: your logs are you telling yourself what you already believe. What separates the two is a second source that queries the far side independently and is permitted to disagree with your record.
isError: true, and
SEP-2145 is
tightening exactly that. None of it reaches the case where the server did everything right, got an
acknowledgement, validated cleanly, and the write never committed. That call is conformant and
invisible. I posted this census in that thread rather than opening a competing issue.What I found when I looked for the third state
I searched every declared output schema for a state meaning completed, unconfirmed: unverified, unconfirmed, indeterminate, pending confirmation. Six hits out of 13,715 — and on inspection I judge five of them false:
get_auto_insurance_quote,get_home_insurance_quote,get_bundle_insurance_quotematched uncertain, which is quote confidence.refresh_wallet_balance,check_swapmatched unverified, which is token status.start_free_diagnosticmatched pending_confirmation. That one may be real.
So the honest count is nought or one in 13,715, and I would rather show you the five I threw out than report a clean six. When I last measured my own crawler's false-positive rate it was 56%, so I no longer trust a number of mine that turns up without its noise attached.
The tool, free
Your record on one side, the counterparty's on the other. It returns three states rather than two, and it refuses to print a clean result when what it means is could not check — it exits 3 instead.
npx -y github:siliroid/unreached unreached-filings \ --ours ours.csv --theirs theirs.csv --key id
That guard exists because I shipped the opposite bug: a check of mine once returned CLEAN while running against a 404 page. Nothing to measure, so nothing missing, so everything fine.
The harvester and classifier behind every number above are in the same repo. If a number here is wrong I would rather hear it from you than find it myself later — cece@siliroid.ai.