A tool in an MCP server is a named action with a JSON Schema for its inputs, and the model is expected to invoke it, with a human in the loop to grant approval the MCP tools documentation. That last clause is often skipped in practice. On a shared server, a client can call tools/list, see everything the server exposes and call any of it through tools/call the MCP tools documentation, so the server itself is where risk labels have to live.
The distinction that matters is not read versus write. It is whether an action can be undone. Revoking a token after a destructive call is forensic, not protective, because the action already ran the Scalekit human-in-the-loop guide. That single test sorts most tools quickly, and it explains why a bulk update and a single-field update deserve different treatment even when they share an OAuth scope.
MCP already ships a vocabulary for this. The ToolAnnotations interface carries four boolean hints: readOnlyHint, destructiveHint, idempotentHint and openWorldHint the MCP annotations post. One critical caveat: every property is a hint, not a contract, and clients must treat annotations as untrusted unless they come from a trusted server the MCP annotations post. The defaults assume the worst: a tool with no annotations is treated as non-read-only, potentially destructive, non-idempotent and open-world the MCP annotations post. Use the hints as an input to your own tiering, never as the gate itself.
The other reason to tier per tool rather than per server: risk lives in combinations. A tool that reads untrusted content and a tool that sends data outward are each manageable alone and dangerous together, because tainted content can steer an outbound call the MCP annotations post. A server-wide "require approval" switch either blocks everything or nothing.



