Skip to main content
Glama
Keldrik
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).