A TypeScript reference implementation of enforced Human-in-the-Loop (HITL) for MCP tools using TOTP (Google Authenticator).
There is an assumption about MCP tools that agent (Claude, Copilot, etc.) can invoke those tools eventually if want to and there is no way to enforce human in the loop (HITL) for critical operations. For example, if you have a tool that signs a transaction or credential with key material, you want to ensure that a human approves the query before it runs every and each time. This small POC project demostrates a pattern to prove MCP tools can be enforced for HITL when needed, without relying on the agent's cooperation or honesty.
Move the approval gate inside the tool itself. The tool literally cannot execute without a valid, human-issued cryptographic proof -- no matter what permissions the agent sets.
Agent calls tool --> Tool checks for human proof --> No proof? Refuse. Period.
Valid proof? Execute.
This POC provides 2 tools that return hardcoded data, but with different security gates:
| Tool | Gate | What's Required |
|---|---|---|
get_color |
None | Nothing -- executes immediately, returns "Dark Blue" |
get_weather |
TOTP | 6-digit code from Google Authenticator |
Turn 1: Agent asks user for OTP code (required parameter)
Turn 2: Human opens Google Authenticator, reads 6-digit code, gives it to agent
Agent calls get_weather({ otp: "482019" })
--> Verified. Weather data returned.
- Node.js 18+
- npm
- Google Authenticator (or any TOTP app) on your phone
git clone https://github.com/GoPlausible/mcp-hitl.git
cd mcp-hitl
npm install
npm run build
cp .env.example .env # Create .env for TOTP secret persistencenpm startOn startup, the server prints to stderr:
- A QR code for Google Authenticator -- scan it to add the TOTP entry
- The TOTP secret (base32) for manual entry
Note: The TOTP secret is generated on first run and persisted to
.env. The same secret is reused across restarts -- scan the QR only once.
Add to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
"mcpServers": {
"mcp-hitl": {
"command": "node",
"args": ["/absolute/path/to/mcp-hitl/dist/index.js"]
}
}
}Restart Claude Desktop. The server's QR code and setup info appear in the MCP server logs (check Developer Tools > Console or the MCP log file).
Testing:
- Ask Claude: "Get the color" -- it will use
get_color(no gate) - Ask Claude: "Get the weather using the OTP-protected tool" -- it will call
get_weather, ask you for the OTP code, then call with it
Add to your project's .mcp.json or global Claude Code MCP config:
{
"mcpServers": {
"mcp-hitl": {
"command": "node",
"args": ["/absolute/path/to/mcp-hitl/dist/index.js"]
}
}
}Or add via CLI:
claude mcp add mcp-hitl node /absolute/path/to/mcp-hitl/dist/index.jsThe QR code appears in stderr during the MCP connection. Use /mcp to verify the server is connected.
- Open VS Code Settings > Cline > MCP Servers
- Add a new server with:
{
"mcp-hitl": {
"command": "node",
"args": ["/absolute/path/to/mcp-hitl/dist/index.js"]
}
}Or edit ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json:
{
"mcpServers": {
"mcp-hitl": {
"command": "node",
"args": ["/absolute/path/to/mcp-hitl/dist/index.js"],
"disabled": false
}
}
}Cline will show tool approval prompts inline. When the OTP gate triggers, Cline will display the APPROVAL_REQUIRED message and you can provide the OTP code in your next message.
- Create a
.vscode/mcp.jsonin your project root:
{
"servers": {
"mcp-hitl": {
"command": "node",
"args": ["/absolute/path/to/mcp-hitl/dist/index.js"]
}
}
}- In VS Code, open Copilot Chat in Agent mode (
@workspace) - Copilot will discover the MCP tools and list them
- Ask Copilot to use the tools -- it will handle the HITL flow
Add to your .opencode/config.json:
{
"mcpServers": {
"mcp-hitl": {
"command": "node",
"args": ["/absolute/path/to/mcp-hitl/dist/index.js"]
}
}
}OpenCode will list the available tools. The HITL flow works the same -- the agent gets the APPROVAL_REQUIRED response and asks you for the OTP code.
- Setup: When the server starts, scan the QR code with Google Authenticator (or manually enter the base32 secret)
- Call the tool: Ask your agent to get weather using the OTP-protected tool
- Agent gets gate response: The tool returns
APPROVAL_REQUIREDwith instructions - Provide OTP: Open Google Authenticator, read the 6-digit code, type it when the agent asks
- Agent re-calls: The tool verifies the OTP and returns weather data
You can test the server directly with the MCP Inspector:
npx @modelcontextprotocol/inspector node dist/index.jsThis opens a web UI where you can call tools directly and see the HITL flow in action.
| Property | OTP |
|---|---|
| Requires physical device | Yes (authenticator app) |
| Time-bound | Yes (30s TOTP window) |
| Replay-proof | Yes (used codes tracked) |
| Brute-force resistant | Yes (1M combinations per 30s window) |
| Offline capable | Yes |
| Agent-unbypassable | Yes (gate is inside the tool) |
mcp-hitl/
src/
index.ts # MCP server with 2 tools + HITL logic
types.d.ts # Type declarations for qrcode-terminal
dist/ # Compiled output (after npm run build)
PLAN.md # Architecture & design document
README.md # This file
package.json
tsconfig.json
This POC intentionally simplifies to demonstrate the core pattern. For production use:
- Persist TOTP secrets in secure storage (vault, encrypted env vars) instead of generating per-session
- Tiered security -- no gate for reads, OTP for writes, multi-factor for high-value operations
- Audit logging -- log all HITL approvals/rejections to an immutable store
- Multi-operator support -- per-operator TOTP secrets
- Rate limiting -- lockout after N failed OTP attempts
MIT -- see LICENSE