Get Started
Approval Gate in MCP hosts
MCP Apps lets an MCP server draw its own interface inside the chat. Our reference server uses it to put
Approval Gate in front of the person at the moment an agent asks to act. The agent's tool call waits
there. It returns once the person approves, rejects or edits the action, and it carries their
ApprovalDecision.
The round trip
-
The agent calls
request_approvalwith the action, its arguments, what it touches and whether it can be undone. The server gives the request an id and version 1, then holds the call open. -
The host sees
_meta.ui.resourceUrion the tool, loadsui://approval-gate/view.htmlinto a sandboxed frame and hands it the tool input. -
The view asks the server for the request it holds through
approval_request, so the person reviews the arguments that will run. The model's wording of them stays out of the gate. -
The person approves, rejects or edits in Approval Gate. The view sends the decision through
approval_decide. -
The server checks the decision against its copy: the same request, the current version (an edit counts
as the next version), a scope it offers, and no earlier decision. It stamps a
serverReceiptIdand releases the waiting call with the decision. The gate's receipt then reads "Verified".
The three view tools carry visibility: ["app"], so the host hides them from the model. Only the
view can record a decision.
The agent's answer
{
"requestId": "req_6f1c…",
"requestVersion": 2,
"outcome": "granted",
"scope": "once",
"editedArgs": { "ids": [101], "status": "closed-lost" },
"decidedAt": "2026-10-04T15:17:51.743Z",
"serverReceiptId": "rcpt_93b0…"
}
This is the ApprovalDecision type from the Approval Gate core, returned as the tool's structured
content. The text content tells the model the rule: run the action only on granted, and with
editedArgs when the person edited it.
Fail closed
- A host without MCP Apps support gets
deniedat once, since the person cannot see the gate there. - An unanswered request expires after five minutes (
APPROVAL_TIMEOUT_MS). - If the agent cancels its call, the request settles as
denied. - The server refuses a stale version, an unknown request or a second decision, and the call keeps waiting.
- While the call waits, the server sends a progress notification at 10-second intervals, so a host that restarts its timeout on progress keeps the call open.
Run it
Node 22.6 or later. From a clone of the repository:
cd examples/mcp-apps-approval npm ci npm run build # bundles the view into dist/view.html npm test # the decision round trip, in memory and over stdio
Open it in the MCP Inspector, then pick the Apps tab and "Ask the person to approve an action":
npm run inspector
Or add it to any host that supports MCP Apps over stdio:
{
"mcpServers": {
"approval-gate": {
"command": "node",
"args": ["--experimental-strip-types", "--no-warnings", "/path/to/examples/mcp-apps-approval/src/main.ts"]
}
}
}
Source, tests and screenshots: examples/mcp-apps-approval. The example pins the MCP
TypeScript SDK 2.3.0, @modelcontextprotocol/ext-apps 2.0.3 and the
MCP Apps spec dated 2026-01-26.
Before you ship it
The example keeps requests in memory in one process, which suits stdio. A remote server needs the requests in shared storage, an authenticated person behind each decision, and an executor that checks the stored grant for the exact version before it runs anything. Revoke only helps if that executor checks again.