Approval Gate
Approval Gate — an MCP App
A human approval gate for AI agents, built as an MCP App. Before an agent does something consequential — send an email, issue a refund, publish a post, delete data — it calls a tool that renders an interactive card inline in the chat. A human clicks Approve, Edit, or Reject, and only then does the agent proceed.
It runs in any MCP Apps host: Claude (web/desktop), ChatGPT, VS Code, Goose, and others.
Why this and not a dashboard? The highest-leverage use of MCP Apps in real workflows isn't a fancy chart — it's the confirmation gate that stops an agent acting on a misread "yes". The artifact changes per client; the pattern doesn't.
How it works
MCP Apps extend MCP with one idea: a tool can declare a UI resource. When the tool is called, the host fetches that resource and renders it in a sandboxed iframe, then relays messages between the UI and your server.
This project wires up that pattern for an approval gate using three tools and one UI resource:
┌─────────────────────────────────────────────┐
agent calls │ request_approval (declares the UI resource)│
request_approval ───▶│ → creates a pending ApprovalRequest │
│ → host renders ui://approval-gate card │
└───────────────────────┬─────────────────────┘
│ card shows the artifact
▼
human clicks Approve / Edit / Reject
│
┌───────────────────────▼─────────────────────┐
the card calls │ submit_approval_decision │
(via the host) ─────▶│ → records the decision (once only) │
│ → returns the outcome to the agent │
└───────────────────────┬─────────────────────┘
│ "APPROVED" / "…WITH EDITS" / "REJECTED"
▼
agent proceeds with the action — or doesn't
list_approvals → read-only audit trail of every gate and its decisionThe agent's contract is simple: call request_approval, then wait. It must not perform the real action until submit_approval_decision reports approved or edited. On edited, it uses the reviewer's corrected values; on rejected, it stops.
Project structure
mcp-approval-gate/
├── server.ts # MCP server: registers the 3 tools + the UI resource
├── main.ts # Entry point: stdio (--stdio) or Streamable HTTP
├── mcp-app.html # View shell (Vite bundles the React app into it)
├── src/
│ ├── types.ts # Shared, type-only model (ApprovalRequest, …)
│ ├── schemas.ts # Zod input schemas for the tools
│ ├── store.ts # In-memory store + audit trail (swap for a DB in prod)
│ └── ui/
│ ├── main.tsx # React entry
│ ├── useApprovalApp.ts# Hook around the ext-apps `App` class (connect/receive/submit/theme)
│ ├── ApprovalCard.tsx # The card: view / edit / reject / outcome states
│ └── styles.css # Theme-adaptive styling (matches host light/dark)
├── vite.config.ts # Bundles the View into a single self-contained HTML file
├── tsconfig.json # Typecheck for the View
├── tsconfig.server.json # Typecheck for the server
└── test-smoke.mjs # End-to-end runtime test over stdioPrerequisites
Node.js 20.19+ or 22+
An MCP Apps-capable host to see the UI (Claude Desktop, ChatGPT, VS Code, Goose, …), or the
basic-hostfrom the ext-apps repo for local testing.
Setup
npm install
npm run build # typechecks, then bundles the View into dist/mcp-app.htmlThe server serves dist/mcp-app.html as the UI resource, so you must build before running.
Run
Streamable HTTP (default, on http://localhost:3001/mcp):
npm startstdio (for Claude Desktop and other local hosts):
npm run start:stdioDuring development, npm run dev rebuilds the View on change and restarts the server.
Use it in Claude Desktop
Build first (npm run build), then add this to your claude_desktop_config.json (use an absolute path):
{
"mcpServers": {
"approval-gate": {
"command": "npx",
"args": ["-y", "tsx", "/ABSOLUTE/PATH/TO/mcp-approval-gate/main.ts", "--stdio"]
}
}
}Restart Claude Desktop, then try a prompt like:
Draft a reply to Jane approving her $40 refund, and ask me to approve it before sending.
The agent calls request_approval, the card appears inline, and your click decides what happens next.
Try it locally with basic-host
The ext-apps repo ships a reference host you can run against this server over HTTP:
# terminal 1 — this project
npm start
# terminal 2 — the reference host
git clone https://github.com/modelcontextprotocol/ext-apps.git
cd ext-apps && npm install && cd examples/basic-host && npm start
# open http://localhost:8080, point it at http://localhost:3001/mcp,
# call request_approval, and interact with the cardTest
test-smoke.mjs boots the server over stdio and drives the full flow — initialize, list tools, call request_approval, read the UI resource, submit an edited decision, verify the one-decision-only guard, and check the audit trail:
npm run build && node test-smoke.mjsAdapting it
Change the artifact, keep the gate.
request_approvaltakes a genericfields: [{ label, value, multiline? }]list, so the same card reviews an email, a refund, a social post, or a config change. Only the calling agent's prompt changes.Make it durable. The store in
src/store.tsis an in-memoryMap— fine for a demo or single instance, lost on restart and not shared across replicas. Back it with Postgres or Redis for production; the function signatures (createRequest,recordDecision,listRequests) are the seam to replace.Add structured output. Each tool currently returns text. Define an
outputSchemaand returnstructuredContentif your host consumes structured tool results.
Tech
TypeScript · React 19 · Vite (single-file bundle) · @modelcontextprotocol/ext-apps · @modelcontextprotocol/sdk. Built against the MCP Apps spec version 2026-01-26.
License
MIT — see LICENSE.