MCP lets a tool describe itself. readOnlyHint: true says "I only read". destructiveHint: true says "I can delete or overwrite something". Clients can use those hints to decide when to ask a person before running a tool.
Two things go wrong with that in practice.
The hint is often missing. We looked at about 20,000 MCP tools for a report on MCP tool annotations. In Docker's MCP Catalog, 69% of tools that delete or overwrite something carry no hint at all.
The hint can be wrong. One server in that catalogue marked 15 delete tools as read-only, "delete ALL integrations" among them. The tools never set a hint, and a framework default filled in readOnlyHint: true for every one. The vendor fixed it within a week; the catalogue still served the old version three months later.
The MCP specification already tells clients what to do about this:
For trust & safety and security, clients MUST consider tool annotations to be untrusted unless they come from trusted servers.
So where should the decision about what an agent may do live? Not in the tool's description of itself. Here's what it looks like to put it somewhere you control instead, with a real run you can reproduce in about five minutes.
The setup
The DVARA MCP quickstart starts a gateway, its console and a small Spring AI MCP server with three tools:
| Tool | What it does |
|---|---|
get_order_status |
Reads an order's status |
lookup_customer |
Returns a customer record, including an email, phone and card number |
delete_customer |
Deletes a customer record. Irreversible. |
Your agent talks to the gateway, not to the server. The gateway lists the server's tools to the agent as demo-tools__<tool> and applies the workspace's rules to every call.
git clone --depth 1 https://github.com/dvarahq/dvara-examples.git
cd dvara-examples/docker-compose/mcp-quickstart
./start.sh
You need Docker and JBang. No licence key and no AI provider account.
The rule
The quickstart loads one policy into the workspace. This is all of it:
version: "1"
rules:
- id: deny-delete
conditions:
mcp_tool:
denylist: [delete_customer]
action: DENY
deny_message: "delete_customer is irreversible and not allowed in this workspace"
Notice what it doesn't depend on: the tool's annotations. If delete_customer claimed to be read-only, or said nothing at all, the rule would apply just the same.
The call
Ask the gateway to run the tool, the way any MCP client would. Replace <your-api-key> with DEMO_API_KEY from the quickstart's .env:
curl -s http://localhost:8080/mcp \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"demo-tools__delete_customer","arguments":{"customerId":"C-42"}}}'
The gateway refuses it before it reaches the server:
{"jsonrpc":"2.0","id":1,"error":{"code":-32001,"message":"delete_customer is irreversible and not allowed in this workspace","data":{"dvara_code":"MCP_POLICY_DENIED","http_status":403,"trace_id":"3054f7e845e4437ea9ec3fb704fe9a0e"}}}
The demo server writes a line to its log whenever delete_customer actually runs. After this call, there's no such line: the server never received it.
The record
Open the console at http://localhost:8090 and go to MCP → Tool Calls. The refused call is there with status 403, next to the allowed get_order_status and the lookup_customer call whose result had its email, phone and card number redacted.
That record is the other half of the point. When someone asks what an agent did, or tried to do, the answer doesn't depend on each server's logging.
Adapting it to your own tools
-
Deny by name. Add the tools that should never run unattended to the
denylist. Start with the ones whose names say delete, drop, revoke, transfer or send. - Hold for a person instead of denying. Some actions should happen, just not without a human saying yes. DVARA can hold a tool call for approval; the native MCP endpoint docs cover how.
- Treat a missing hint as unknown. If a tool you haven't classified shows up, the safe default is to ask, not to assume it only reads.
- Pin your servers. Catalogues can serve an older version than upstream. Know which version you run.
Annotations are still worth setting if you write MCP servers: they help every client that does read them. Just don't make them the thing that stands between an agent and an irreversible action.
Data in this post comes from our report on MCP tool annotations, which has the full method. Tool data: Glama (glama.ai); Docker MCP Catalog data is MIT-licensed.
Top comments (0)