// Research · 2026-09-12
The name is not the tool.
An MCP guard sits between an agent and a server and decides, call by call, what to inspect. To do that it has to answer one question first: is this the tool that moves money? Every implementation we know of — including ours — answers by matching the tool's name against a list.
The name belongs to the server. It is chosen by whoever wrote the tool, it is not standardised, and nothing requires it to resemble anyone else's. So we measured the vocabulary a name list actually has to cover. 590 distinct tools across 601 servers place or settle value, and 88.1% of those names appear on exactly one server.
We found this by testing our own published package and watching it forward a $5,000 order while printing a $100 cap.
The defect, in our own software
mcp-trade-guard caps what an AI trading
agent can spend. It ships a list of order-tool names — place_order, buy, sell and three more — written against one
broker's vocabulary and never verified against it, because the endpoint requires
credentials we do not have. We sent nine plausible order-tool names through the real dispatch path with
a $5,000 order and a $100 cap.
place_orderREFUSEDplace_equity_order_v2REFUSEDrh_place_orderREFUSEDorders.createFORWARDED — $5,000, uncappedcreateOrderFORWARDED — $5,000, uncappedtradeFORWARDED — $5,000, uncappedplace_market_orderFORWARDED — $5,000, uncappedsubmit_equity_orderFORWARDED — $5,000, uncappedorderFORWARDED — $5,000, uncappedFive
of nine reached the broker untouched. The failure is not that the guard blocked the wrong thing — it is
that the guard reported itself healthy while inspecting
nothing. The startup banner still read per-order $100. A tool that says it is
protecting you while passing every trade is worse than no tool, because it ends the search for one.
Note
that place_equity_order_v2
and rh_place_order were
caught — by substring matching, not by design. The same permissiveness that saved those two is what
makes the approach unsound in the other direction, as the false positives below show.
What the registry actually contains
To find out how wide the vocabulary is, we enumerated the official MCP registry and asked every reachable remote server for its tool list.
101,88842,54642,500 (99.9%)22,45811,595 (27.3%)500,83178,857The registry has grown roughly fourfold since our last scan — 101,888 entries against 26,844 in early September. The 42,500 endpoints probed here are 99.9% of those exposing a remote URL; 46 were not reached before the run ended, and the figures below are stated over what was probed rather than extrapolated.
590 tools that move money
A tool that moves money is not the same as a tool whose name contains a money word. We used three levels of rigour on the same corpus.
776
A money noun (order, payment, checkout, transfer, swap) with an action verb and no read-only verb. This is the instrument an MCP guard actually runs.
590
Of those, the ones whose own description does not say they advise, simulate, quote, or return a link. 186 did — a 24.0% false-positive rate for the name alone.
79
What mcp-trade-guard 0.1.0 would actually have inspected. Everything else reached the broker with no cap applied.
The middle step is the one that matters for honesty. 24.0% of the names that look like they move value do not, and their own descriptions say so:
inspect_swap
“FREE. Deterministically inspect a narrow Base USDC-to-WETH swap against explicit policy. No wallet act…”
agent_can_pay
“Free pre-payment policy decision. Agents ask whether an x402 endpoint should be paid…”
rsu_sell_vs_hold
“Use this when someone asks whether to sell RSUs at vest or hold them…”
anime_buy_wait
“Return BUY, WAIT, WATCH or AVOID for an identified Japanese anime collectible…”
swap_case
“Swap case of each character.”
swap_case swaps the case of characters
in a string. A keyword scanner has no way to know that, which is the same result we published when
measuring tool poisoning: the name is a weak signal, and a scanner that trusts it over-reports.
The vocabulary has no centre
If
the 590 names clustered around a few conventions, a list would work. They do not. 520 of them (88.1%) appear on exactly one server, and
covering 80% of server-tool pairs takes 418 distinct names. These all place or settle value, and none of
them is matched by a list built from place_order:
create_checkout
Create the order and get a hosted payment link for a chosen offer.
purchase_confirm_intent
Confirm a purchase intent — re-validates price within 1p tolerance.
complete_checkout
Complete checkout with payment and start site provisioning.
services_purchase
Buy a fixed-price service in one call: validates the scope, computes…
batch_purchase
Buy up to 20 SPECIFIC listed ENS names in a SINGLE Seaport transaction.
write_account_withdraw
Withdraw from an account.
transfer_out_domain
Prepare a domain to transfer to another registrar.
place_spot_order
Place a spot order.
There
is no shared root to anchor on. create_checkout, purchase_confirm_intent and
write_account_withdraw describe the
same class of action in three unrelated vocabularies. One is in Korean. A guard maintained by watching
the ecosystem is a guard permanently one server behind it.
The protocol already has an answer, and it is not used well
MCP
defines annotations,
including readOnlyHint — a
machine-readable declaration that a tool does not modify anything. 61.8% of tools carry annotations and
220,853 declare readOnlyHint.
That is genuinely useful, and it is the signal we now prefer over the name. But it is a hint, supplied by the same party that chose the name, and a hostile server can set it freely. It narrows what a guard must inspect; it cannot be what a guard trusts.
What we changed
The fix is not a longer list. A longer list fails the same way, later. What was missing was any signal that the guard had matched nothing — the silence looked identical to safety.
Since
0.2.0 the proxy reads the
server's own tools/list
response, reconciles it against the vocabulary it is running with, and prints every advertised
tool that looks like it moves money but would not be intercepted. If it matches none of them, it exits
rather than run:
!! UNGUARDED TOOLS — these move money and are NOT capped:
orders.create
Add them: MCP_ORDER_TOOLS="orders.create"
!! This guard matched NONE of the 2 tools this server advertises.
Refusing to run as a guard that guards nothing.
This does not solve name-based interception. It converts a silent failure into a loud one, which is the most a guard can honestly do about a vocabulary it does not control. The operator still has to supply the real names — but now they are told, at startup, that they must.
Limits
The classifier is ours, and it is imperfect. Tier 1 is a regex over names; tier 2 reads descriptions for advisory language. Both will misjudge some tools. We report the 24.0% false-positive rate between them precisely so the tier-1 number is not mistaken for a measurement.
27.3% of endpoints require authentication and were not enumerated. Money-moving tools are more likely to sit behind auth, so 590 is a floor, not a total.
The scan covered 99.9% of remote endpoints, not all of them. 46 endpoints were not reached. Nothing here is extrapolated to them.
We can only prove this on software we control. We believe name matching is how other MCP guards decide what to inspect, because it is the only signal available before a call is forwarded. We have not audited anyone else's, and we are not claiming a specific competitor is vulnerable.
Repeating this
Enumerate registry.modelcontextprotocol.io/v0/servers,
take every remotes[].url, and send
each one initialize
then tools/list over
streamable HTTP. Two details cost us time: the response may arrive as an SSE frame rather than JSON even
for a plain POST, and the session id returned by initialize must be echoed on the next
request or the listing comes back empty.
The
guard probe is nine lines against the published package: npm i mcp-trade-guard, construct a guard with a
$100 cap, and call isOrderTool
with each name before deciding whether guardOrder ever runs. Version
0.1.0 reproduces the five
forwards; 0.2.0 reports them.