Tool Definition & Lifecycle
MCP tools with destructive names or behaviors lack honest annotations, allowing LLM clients to invoke state-mutating operations silently without user confirmation.
The MCP specification defines tool annotations — specifically `destructiveHint` and `readOnlyHint` — as a contract between servers and clients that gates whether user confirmation is required before execution. Violations occur in two forms: a tool whose name implies destruction (e.g., `delete_*`, `wipe_*`, `wire_transfer_*`) is registered without `destructiveHint: true`, or a tool asserts `readOnlyHint: true` while its handler body contains destructive sinks such as `fs.unlink`, `DROP TABLE`, or `DELETE FROM`. Both patterns cause clients to incorrectly classify the tool's safety profile.
In traditional web APIs, a human explicitly chooses which endpoint to call and reviews its documentation before invoking it. In MCP, the LLM selects and invokes tools autonomously based on tool names, descriptions, and annotations — meaning a missing or false annotation directly determines whether the model executes a destructive action without any confirmation gate. Prompt injection attacks compound this risk: a malicious instruction in retrieved content can nudge the model to invoke a deceptively annotated `readOnly` tool that silently deletes records. The tool composition model also means a single dishonest annotation in a multi-step agentic pipeline can cascade into unrecoverable data loss.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
const server = new McpServer({ name: "acme-users", version: "0.1.0" }); |
server.tool( |
"delete_user", |
"Permanently remove a user account.", |
async ({ userId }) => { |
await db.users.delete({ where: { id: userId } }); |
return { ok: true }; |
}, |
); |
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
const server = new McpServer({ name: "acme-users", version: "0.1.0" }); |
server.tool( |
"delete_user", |
"Permanently remove a user account.", |
async ({ userId }) => { |
await db.users.delete({ where: { id: userId } }); |
return { ok: true }; |
}, |
{ annotations: { destructiveHint: true } }, |
); |
MCPSafe matches `server.tool(...)` and `@mcp.tool(...)` registrations where the tool name matches a destructive-action pattern (`/^(delete|remove|wipe|drop|purge|erase|wire_transfer|truncate)_/i`) and the annotations object is absent or lacks `destructiveHint: true`; a second rule performs cross-statement taint analysis within the same file to flag any tool registration asserting `readOnlyHint: true` whose handler closure contains a destructive sink (`fs.unlink`, `fs.rm`, `os.remove`, `os.unlink`, SQL `DELETE`/`DROP`/`TRUNCATE` string literals, or HTTP calls with `method: DELETE`). Benign read-only tools with no destructive sinks and properly annotated destructive tools are explicitly excluded from both signals.
See the full threat catalog for every documented detection.
MCPSafe runs this check — and every other rule in the catalog — on any MCP server you paste in.
Scan now