Approval Gate
by Keldrik
README.md
# Approval Gate — an MCP App
A human **approval gate** for AI agents, built as an [MCP App](https://apps.extensions.modelcontextprotocol.io/api/). 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 decision
```
The 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 stdio
```
## Prerequisites
- Node.js 20.19+ or 22+
- An MCP Apps-capable host to see the UI (Claude Desktop, ChatGPT, VS Code, Goose, …), or the `basic-host` from the ext-apps repo for local testing.
## Setup
```bash
npm install
npm run build # typechecks, then bundles the View into dist/mcp-app.html
```
The 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`):
```bash
npm start
```
stdio (for Claude Desktop and other local hosts):
```bash
npm run start:stdio
```
During 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):
```json
{
"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:
```bash
# 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 card
```
## Test
`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:
```bash
npm run build && node test-smoke.mjs
```
## Adapting it
- **Change the artifact, keep the gate.** `request_approval` takes a generic `fields: [{ 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.ts` is an in-memory `Map` — 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 `outputSchema` and return `structuredContent` if 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](LICENSE).
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues