Read-only vs trade permission in an MCP setup
Figures on this page are as of 2026-09. Fees, limits and margin tiers change — check the venue's own docs before acting on a number.
“Read-only” in an MCP setup can mean two completely different things: a label the server attaches to its own tools, or a permission the venue enforces on your key. Only one of them survives the server being wrong.
This distinction is easy to miss because both are called read-only, both appear in configuration, and in the normal case they agree. They come apart exactly when it matters.
The annotation is a hint, and the spec says so
MCP lets a server attach annotations to each tool it exposes. One of them is
readOnlyHint. The schema defines it plainly:
readOnlyHint?: boolean— “If true, the tool does not modify its environment.” Default:false.
So far so good. Now the note directly above it in the same schema file:
“NOTE: all properties in ToolAnnotations are hints. They are not guaranteed to provide a faithful description of tool behavior (including descriptive properties like
title).Clients should never make tool use decisions based on ToolAnnotations received from untrusted servers.”
And the tools specification repeats it normatively:
“For trust & safety and security, clients MUST consider tool annotations to be untrusted unless they come from trusted servers.”
The implication for trading is blunt. Nothing stops a server from declaring a
place_order tool with readOnlyHint: true. The annotation is not verified
by anything; it is the server describing itself. A client that decides what to
auto-approve by reading annotations is asking the component it is supposed to be
guarding for permission to stop guarding it.
This is not a flaw in MCP. Annotations exist to improve UI and model behaviour, and the spec is honest about what they are. It is a flaw in using them as a security boundary.
The defaults are better than people assume
Worth knowing, because it changes how you read a server that annotates nothing:
| Field | Default | What the default means |
|---|---|---|
readOnlyHint | false | Assume the tool modifies state |
destructiveHint | true | Assume the modification is destructive |
idempotentHint | false | Assume calling twice does it twice |
openWorldHint | true | Assume it touches external systems |
An unannotated tool is, by the specification’s own defaults, treated as destructive and non-idempotent. That is the right default, and it means a server that annotates nothing is easier to reason about than one that annotates optimistically. Absence of annotations is not a red flag. Confident annotations from a server you have not audited are.
Note idempotentHint defaulting to false in particular. For an order-placement
tool that default is correct and load-bearing: calling it twice places two
orders.
Where read-only can actually be enforced
Three layers, in increasing order of how much they are worth.
1. The server’s own configuration
Some servers implement real read-only modes, and the good ones are quite
thorough. Among the Bybit community servers, one ships with trading disabled
by default, requires an explicit TRADING_ENABLED=true to enable mutating
tools, offers a READONLY_MODE=true that hard-blocks mutations, and caps
estimated order notional with a MAX_ORDER_SIZE_USDT setting. Another is
read-only by construction and recommends only ever pointing a read-only API key
at it.
This is genuinely good engineering and you should prefer servers that do it. But it is a control inside the component you are bounding. It protects you from the model; it does not protect you from the server being buggy, being updated, or being something other than what you thought you installed.
2. The venue’s key permissions
This is the layer that holds regardless of what the software does, because it is enforced on the other side of the network by a party with no stake in your configuration being right.
On a centralised exchange this is the API key’s permission set. The detail worth knowing on Binance: withdrawals cannot be enabled at all on a key without IP access restriction — the filter is a precondition, not an option alongside it. And a key with no IP whitelist has its spot trading permission automatically switched off after a period, by design, as a standing hygiene mechanism.
The practical shape: a key that can read and nothing else cannot be talked into placing an order by any prompt, any server bug, or any annotation. There is no argument to be had with the exchange.
Binance also exposes the key’s current state programmatically via
GET /sapi/v1/account/apiRestrictions, which returns fields including
ipRestrict, enableReading, enableWithdrawals and enableFutures. If you
want to verify what a key can do rather than remember what you ticked, ask.
3. Local code between the model and the venue
The layer the venue cannot provide, because the venue does not know your rules. Per-trade size caps, a daily-loss breaker, a cooldown after consecutive losses — these are yours, and they only bind if they are evaluated as functions rather than read as instructions.
The case that breaks the mental model
Everything above assumes a key with permissions attached to it. Hyperliquid does not work that way, and it is worth knowing before you carry CEX advice across.
There is no API key. A master account approves an API wallet (also called an agent wallet) to sign on its behalf. The documentation is explicit that “API wallets are only used to sign”, and that to query account data you must pass the actual address of the master or sub-account — the agent’s own address owns nothing.
Can that agent wallet move funds? Hyperliquid’s documentation does not say
either way; there is no prohibition to quote. What its official Python SDK
shows is structural: the fund-moving actions (withdraw3, usdSend,
spotSend) are user-signed and take no account or vault address, whereas
trading actions go through the L1 signing path that does carry one. That
address field is the mechanism by which an agent’s signature is attributed to
the master. Without it, there is no way to express “move the master account’s
funds” in the signed payload at all.
So the practical answer is that the signing scheme gives an agent wallet no way to express a withdrawal of the master’s funds — a structural property rather than a permission flag. That is a meaningfully different kind of guarantee from a checkbox, and arguably a stronger one. But Hyperliquid has not stated it, and you should verify it against your own setup before relying on it as a security boundary.
What to actually do
- Treat
readOnlyHintas a UI affordance. It is useful for making a client’s interface sensible. It is not a permission. - Set the permission at the venue, and prefer a key that cannot trade until you have decided you want it to.
- Prefer servers with explicit default-off trading and a real read-only mode — then still set the key permission, because the two protect against different failures.
- Re-check after updates. A server’s tool list can change under you; the
protocol has a
notifications/tools/list_changedmessage precisely because this is expected behaviour.
FAQ
If a tool says readOnlyHint: true, can it still place an order?
Yes. The annotation is a claim the server makes about its own tool, and the MCP schema states outright that annotations “are not guaranteed to provide a faithful description of tool behavior”. The specification further requires clients to treat annotations from untrusted servers as untrusted. Use the annotation to build a sensible interface; use the venue’s key permissions to decide what is actually possible.
Is a server that provides no annotations less safe?
No, and slightly the opposite. The schema defaults assume the worst —
readOnlyHint false, destructiveHint true, idempotentHint false — so an
unannotated tool is treated as destructive and non-idempotent. A server that
annotates nothing gives you conservative defaults. A server that annotates
confidently gives you claims you would have to audit to trust.
Can I use one API key for both reading and trading if I am careful?
You can, and the question is what “careful” is protecting against. A single key with trade permission means every path to that key — the server, the client, the model, anything that can read your environment variables — is a path to an order. Separate keys collapse that to one path. The operational cost is configuring two credentials once.
How do I check what a key is actually allowed to do?
Ask the venue rather than your memory. On Binance,
GET /sapi/v1/account/apiRestrictions returns the current permission flags
including ipRestrict, enableReading and enableWithdrawals. Other venues
expose equivalents to varying degrees. Checking takes one request and catches
the case where a key is not configured the way you believe.