docs/apps/providers/approval.mdx
import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.2.0" />Approval adds a human-in-the-loop confirmation step to any server. The LLM presents what it's about to do, the user approves or rejects via buttons, and the decision flows back into the conversation as a message.
from fastmcp import FastMCP
from fastmcp.apps.approval import Approval
mcp = FastMCP("My Server")
mcp.add_provider(Approval())
This registers a single tool:
| Tool | Visibility | Purpose |
|---|---|---|
request_approval | Model | Shows an approval card, sends the user's decision back as a message |
The LLM calls request_approval with a summary (and optional details) whenever it's about to take a significant action. The user sees a card with Approve and Reject buttons. Clicking either sends a message back into the conversation via SendMessage, which triggers the LLM's next turn.
The message looks like it came from the user:
"Deploy v3.2 to production" — I selected: Approve
The constructor sets defaults; the LLM can override all of these per-call via tool arguments.
Approval(
name="Approval", # App name
title="Approval Required", # Card heading
approve_text="Approve", # Approve button label
reject_text="Reject", # Reject button label
approve_variant="default", # "default", "destructive", "success", "info"
reject_variant="outline", # same options plus "outline"
)
The LLM can customize each invocation:
request_approval(
summary="Delete 47 files from /tmp",
details="This cannot be undone.",
title="Destructive Action",
approve_text="Delete",
approve_variant="destructive",
reject_text="Keep files",
)
When the user clicks a button, two things happen:
SendMessage pushes the decision into the conversation as a user messageSetState("decided", True) replaces the buttons with "Response sent."The tool description instructs the LLM to stop and wait for the "I selected:" message before proceeding. If approved, it continues. If rejected, it acknowledges and asks how to proceed.