Stop your AI agent deleting things: human approval for destructive MCP tools A developer released mcp-safe-actions, an open-source TypeScript library that wraps destructive MCP tools so they cannot run until a human approves. The library splits each dangerous tool into a side-effect-free preview and an execute step gated either by MCP client elicitation or a single-use, expiring token bound to the same tool and arguments, and it ships a mutation-testing suite that deliberately breaks the safety code in 10 ways, all of which the tests catch. Your AI agent should not be able to delete a file, send an email or drop a table just because it decided to. "Please ask first" in a system prompt is a request. It is not a control. If you build MCP Model Context Protocol servers, you can enforce the rule in code. I wrote a small TypeScript library, mcp-safe-actions , that wraps a dangerous tool so it does nothing until a human approves. Repo: https://github.com/techsaurabh-394/mcp-safe-actions https://github.com/techsaurabh-394/mcp-safe-actions A prompt-injected or confused model can call delete file "../../important" . Two things are true at once: You split a dangerous tool into two functions. preview describes what will happen and changes nothing. execute runs only after approval. js import { rmSync, statSync } from "node:fs"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { registerConfirmedTool, fileAudit } from "mcp-safe-actions"; const server = new McpServer { name: "files", version: "1.0.0" } ; registerConfirmedTool server, { name: "delete file", description: "Permanently delete one file.", inputSchema: { path: z.string }, preview: { path } = { summary: Permanently delete "${path}" ${statSync path .size} bytes , details: "This cannot be undone." , } , execute: { path } = { rmSync path ; return { content: { type: "text", text: Deleted ${path} } }; }, audit: fileAudit "./audit.jsonl" , } ; await server.connect new StdioServerTransport ; The tool is listed with destructiveHint: true . Client supports elicitation: the server asks the human through the client UI. The model never sees the approval step, so it cannot approve itself. Declined, cancelled, timed out or crashed means nothing runs. Client does not support elicitation: the server returns the plan plus a one-time token. The assistant has to show the plan to the user and call again with the token. The token is single-use, expires, and only works for the same tool with the same arguments. The mode option picks the behaviour: auto default , elicit or token . Safety code needs tests that try to break it. From the repo: src/ . npm run mutation that breaks the code on purpose in 10 ways, such as a reusable token, approval skipped on error, or secrets in the log. It fails if the tests do not notice. All 10 are caught. The mutation check also exposed one weak test of mine. It is written up in docs/TESTING.md . mode: "elicit" . execute should re-check state that changed since the preview. npm install github:techsaurabh-394/mcp-safe-actions . git clone https://github.com/techsaurabh-394/mcp-safe-actions.git cd mcp-safe-actions npm install npm run demo prints both approval flows npm test Approval channel hooks Slack, webhook for headless servers, a policy mode that auto-approves low-risk calls and always asks for high-risk ones, and a Python version for FastMCP servers. If you build MCP servers, I would like to know which of your tools you would not let a model call unattended, and what approval flow you would want for them. Issues and PRs are welcome.