mcp-safe-actions
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-safe-actionsdelete the old logs, but show me a preview and wait for my approval"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-safe-actions: human approval for destructive MCP tools
Your AI agent should not be able to delete a file, send an email or drop a table just because it decided to.
mcp-safe-actions is a small TypeScript library for people who build MCP servers (Model Context Protocol). Wrap a dangerous tool once and it does nothing until a human approves. The model cannot approve its own action.
Why you might want this
A prompt-injected or confused model can call
delete_file("../../important"). "Please ask first" in a prompt is a request, not a control.Approval in the client UI is the right place for a human decision, and MCP now has a feature for it (elicitation). Not every client supports it yet, so you also need a safe fallback.
You want a log of what was previewed, approved, refused and run, without leaking secrets.
This library packages that pattern once, with a test suite that proves the safety rules, instead of rebuilding it in every server.
Related MCP server: ShadowRun
Add it to your MCP server in 4 steps
1. Install
npm install github:techsaurabh-394/mcp-safe-actionsOnce it is published to npm: npm install mcp-safe-actions.
You also need @modelcontextprotocol/sdk (1.17+) and zod (you probably have both). Node 18+.
2. Wrap your dangerous tool
Replace server.registerTool(...) with registerConfirmedTool(server, ...) and split the work into preview (describe, don't change anything) and execute (runs after approval):
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() },
// What WILL happen. Must not change anything. Be specific.
preview: ({ path }) => ({
summary: `Permanently delete "${path}" (${statSync(path).size} bytes)`,
details: ["This cannot be undone."],
}),
// Runs only after the user approved. Re-check anything that could have changed.
execute: ({ path }) => {
rmSync(path);
return { content: [{ type: "text", text: `Deleted ${path}` }] };
},
audit: fileAudit("./audit.jsonl"), // optional
});
await server.connect(new StdioServerTransport());The tool is listed with destructiveHint: true and a description telling the model to wait for the user.
3. Run the demo and the tests
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 # 49 tests4. Connect it to an MCP client
Claude Desktop (claude_desktop_config.json), using the included example server and a throwaway folder:
{
"mcpServers": {
"files": {
"command": "node",
"args": ["/absolute/path/to/mcp-safe-actions/dist/examples/files-server.js"],
"env": { "FILES_DIR": "/absolute/path/to/test-folder", "AUDIT_LOG": "/absolute/path/to/audit.jsonl" }
}
}
}Claude Code: claude mcp add files -- node /absolute/path/to/mcp-safe-actions/dist/examples/files-server.js
Run npm run build first. Then ask the assistant to delete a file in the test folder. You should see either an approval prompt (clients with elicitation) or a preview with a token and a question from the assistant (clients without). The file must still exist until you say yes.
How it works
Client supports elicitation (best case):
sequenceDiagram
participant M as AI model
participant S as Your MCP server
participant H as Human (client UI)
M->>S: delete_file("report.pdf")
S->>S: preview() builds the plan, nothing changes
S->>H: Approval prompt: delete report.pdf?
H-->>S: Approve or decline
alt Approved
S->>S: execute() and audit log
S-->>M: Deleted report.pdf
else Declined or no answer
S-->>M: User did not approve. Nothing changed.
endClient without elicitation (token fallback):
sequenceDiagram
participant M as AI model
participant S as Your MCP server
participant H as Human (in chat)
M->>S: delete_file("report.pdf")
S-->>M: Nothing changed yet. Plan + one-time token
M->>H: Shows plan, asks to go ahead
H-->>M: Yes
M->>S: delete_file("report.pdf", confirm_token)
S->>S: token valid, unused, same arguments, not expired
S->>S: execute() and audit log
S-->>M: Deleted report.pdfDecision table
Situation | Result |
Elicitation accepted and the box is ticked |
|
Declined, cancelled, unticked, timed out or errored | Nothing runs |
| Refused, no token issued |
Token flow, no token | Preview plus one-time token, nothing runs |
Token flow, valid token and identical arguments |
|
Reused, expired, unknown, or for other arguments or another tool | Nothing runs, reason returned |
Modes
| Behaviour | Use when |
| Elicitation if supported, otherwise token | Most servers |
| Elicitation only | Actions that must have a human gate |
| Always the token flow | Testing, or by preference |
Proof it works: tests that try to break it
49 tests | Unit, end-to-end (a real MCP client and server in memory), property tests (300 random argument objects), and real stdio tests that spawn the example server as a child process |
100% line coverage of the library ( |
|
10 deliberate bugs, all caught |
|
What the tests prove, among other things: a declined prompt never runs the action; a model-supplied token cannot skip the human prompt; tokens are single-use, expire, and only work for the same tool and arguments; two simultaneous uses of one token run the action exactly once; a prompt that times out or crashes fails closed; secret argument values never reach the audit log; the example server blocks path traversal and symlink escapes.
Details and an honest note about a weak test the mutation check caught: docs/TESTING.md.
Guarantees, and what it does NOT guarantee
It guarantees | How |
Nothing runs before approval |
|
The model can't self-approve (elicitation) | The prompt goes to the human through the client, not through the model |
Tokens are single-use, bound to tool and exact arguments, and expire | Checked server-side |
Failures fail closed | Errors and timeouts never fall through to execution |
Audit without leaking secrets | Logs your summary and a hash of the arguments, never the raw arguments |
Be clear-eyed:
Token mode is a speed bump, not proof of a human. A model could call twice without asking. For actions that need a real human gate, use
mode: "elicit".Your preview must be truthful, and
execute()should re-check state that may have changed since.It doesn't sandbox your server. It gates the tools you wrap. Keep least-privilege credentials and validate inputs.
Elicitation depends on the client. Support differs by client and version.
Full threat model: docs/SECURITY-MODEL.md.
API
registerConfirmedTool(server, {
name, title?, description, inputSchema, // like server.registerTool
preview(args) => { summary, details? }, // no side effects
execute(args) => CallToolResult, // after approval
risk?: "destructive" | "write", // default "destructive" (sets annotations)
mode?: "auto" | "elicit" | "token", // default "auto"
approvalTimeoutMs?: number, // default 5 minutes; a timeout means "not approved"
store?: new ApprovalStore({ ttlMs, maxPending }),
audit?: (event) => void | fileAudit(path),
annotations?, // override tool annotations
});confirm_token is a reserved input name. More in docs/USAGE.md, including how to test your own tools with an in-memory client.
GitHub Actions in this repo
flowchart LR
A[Push or pull request] --> B[CI]
B --> B1[Build and 49 tests on Node 18, 20, 22]
B --> B2[Demo runs end to end]
B --> B3[Coverage report]
B --> B4[Mutation check: 10 deliberate bugs]
B --> B5[SVG diagrams valid]
C[Tag v*] --> D[Release]
D --> D1[Tests]
D1 --> D2[GitHub Release with notes]
D2 --> D3[npm publish with provenance, only if enabled]Workflow | Runs when | Does |
| every push to | Build, tests on Node 18/20/22, demo, |
| you push a tag like | Tests, GitHub Release, and npm publish if you enabled it |
| monthly | Opens update PRs for npm packages and Actions |
Add the same safety net to your own server's repo:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
- run: npm ci
- run: npm test # include a test that the dangerous tool does NOT run without approvaltest/helpers.ts in this repo shows how to simulate a human who approves, declines, or never answers.
Roadmap
Elicitation flow, token fallback, timeouts, audit log, annotations
49 tests, 100% line coverage, mutation check, real stdio tests
Publish to npm
Approval channel hooks (Slack, webhook) for headless servers
Policy: auto-approve low-risk calls, always ask for high-risk ones
Python version for FastMCP servers
Contributing
Issues and PRs welcome: CONTRIBUTING.md. Any change to approval logic needs a test showing the action does not run without approval, and npm run mutation must still pass.
Star history
Support this project
If this helped you ship a safer agent, please star the repo, share it, or buy me a coffee:
License
MIT. See LICENSE. Copyright (c) 2026 Saurabh Pathak.
This server cannot be deployed
Maintenance
Related MCP Connectors
Preflight, approve, and prove consequential agent actions with signed evidence and x402 tools.
Guard wraps stdio MCP servers: risky calls wait for a person; no answer, no run. Remote: cooperative
MCP server for mandates, delegation, policy-gated execution, credential grants, and audit.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Related MCP Servers
- AlicenseBqualityCmaintenanceSecurity gateway that wraps any MCP server with per-tool policies, approval gates, and optional Ed25519-signed decision receipts. Shadow mode logs every tool call without blocking; enforce mode applies block, rate-limit, and minimum-tier rules. Receipts are independently verifiable offline with no accounts needed.5653 npm11MIT
- AlicenseNot gradedqualityBmaintenanceA local-first stdio proxy for MCP servers that traps destructive tool calls and holds them for manual commit or discard, preventing accidental mutations.AGPL 3.0
- AlicenseNot gradedqualityBmaintenanceMCP server that enforces single-use human confirmations for sensitive tool calls, sanitizes prompts against prompt injection, and re-validates file paths before destructive actions.30 PyPIMIT
- AlicenseNot gradedqualityCmaintenanceEnforces deterministic security policies as an inline firewall for MCP server tool calls, with AST-based validation, cryptographic audit logging, and CLI-based evaluation and verification.MIT