Chat - Tool Approval
Add human-in-the-loop checkpoints to review and approve or deny agent tool calls before they execute.
Tool approval lets you pause the agent when it requests a potentially dangerous action, present the user with an approve/deny interface, and resume or cancel the tool execution based on the user's decision.
Approval workflow
The approval lifecycle extends the standard tool invocation states with two phases:
| State | Description |
|---|---|
input-streaming |
Tool input JSON is being streamed |
input-available |
Tool input is fully available |
approval-requested |
Stream pauses; user approval is needed |
approval-responded |
User has responded, stream continues |
output-available |
Tool output is ready (if approved) |
output-denied |
User denied the tool call |
The stream pauses at approval-requested until your UI calls addToolApprovalResponse().
Approving a tool call in action
Send the suggested prompt; the stream pauses at approval-requested until you approve or deny the call.
Triggering an approval request
When the backend determines a tool call needs user approval, it sends a tool-approval-request chunk (see Building an adapter for the server side):
controller.enqueue({
type: 'tool-approval-request',
toolCallId: 'call-1',
toolName: 'delete_files',
input: { path: '/tmp/data', recursive: true },
approvalId: 'approval-1', // optional, defaults to toolCallId
});
| Field | Type | Description |
|---|---|---|
toolCallId |
string |
The tool invocation being gated |
toolName |
string |
Name of the tool requesting approval |
input |
ChatToolInput<TToolName> |
The tool's input (typed per tool; unknown for dynamic tools), shown to the user |
approvalId |
string | undefined |
Optional distinct approval ID. Stored on the invocation as toolInvocation.approvalId; respond with it when present, otherwise with toolCallId. |
dynamic |
true | undefined |
Set to true when gating a dynamic (unregistered) tool; required on dynamic-tool approval chunks. |
When this chunk arrives, the tool invocation moves to state: 'approval-requested'.
Implementing approval responses
Implement addToolApprovalResponse on your adapter to send the user's decision to the backend:
interface ChatAddToolApproveResponseInput {
id: string; // the chunk's approvalId when provided, otherwise the toolCallId
approved: boolean; // true = approved, false = denied
reason?: string; // optional reason surfaced to the model when denied
}
async addToolApprovalResponse({ id, approved, reason }) {
await fetch('/api/tool-approval', {
method: 'POST',
body: JSON.stringify({ id, approved, reason }),
});
},
Two behaviors are worth calling out:
reasonis stored on the invocation astoolInvocation.approval.reason, and the built-in tool widget displays it when the call ends inoutput-denied; otherwise it is only sent to your backend. The built-in Deny button sends no reason — provide a custom renderer (see Registering custom renderers for tool approval) to collect one.- If your adapter's
addToolApprovalResponserejects, the built-in widget re-enables its buttons and the error is reported through the chat error channel (onError); the invocation stays inapproval-requestedso the user can retry.
Responding to approval requests in the UI
Use useChat() to call addToolApprovalResponse from your component:
const { addToolApprovalResponse } = useChat();
// Approve
await addToolApprovalResponse({
id: toolCall.approvalId ?? toolCall.toolCallId,
approved: true,
});
// Deny with reason
await addToolApprovalResponse({
id: toolCall.approvalId ?? toolCall.toolCallId,
approved: false,
reason: 'User declined the operation',
});
After responding, the tool invocation moves to state: 'approval-responded', and the stream continues. If approved, the tool proceeds to execution and eventually reaches output-available. If denied, the tool moves to output-denied.
Prompting the user for confirmation
ChatConfirmation renders a prominent warning card with a message and two action buttons for human-in-the-loop checkpoints:
import { ChatConfirmation } from '@mui/x-chat';
<ChatConfirmation
message="Are you sure you want to delete all files?"
onConfirm={() => agent.confirm()}
onCancel={() => agent.cancel()}
/>;
Interactive playground
Preview the ChatConfirmation UI in different states:
ChatConfirmation
A confirmation card for gating a tool call before it runs.Custom labels
Use confirmLabel and cancelLabel to tailor the button text to the action:
<ChatConfirmation
message="Send this email on your behalf?"
confirmLabel="Send email"
cancelLabel="Cancel"
onConfirm={handleConfirm}
onCancel={handleCancel}
/>
Connecting to the adapter
Hold the card visibility in React.useState. Show the card when the agent triggers a confirmation step, and hide it once the user responds:
const [pendingConfirmation, setPendingConfirmation] = React.useState(false);
const setConfirmRef = React.useRef(setPendingConfirmation);
setConfirmRef.current = setPendingConfirmation;
const adapter = React.useMemo(
() => ({
async sendMessage({ message }) {
// ... stream agent response ...
// Signal that a confirmation is needed:
setConfirmRef.current(true);
return responseStream;
},
}),
[],
);
// In your JSX:
{
pendingConfirmation && (
<ChatConfirmation
message="Proceed with this action?"
onConfirm={() => {
setPendingConfirmation(false);
/* approve the tool */
}}
onCancel={() => setPendingConfirmation(false)}
/>
);
}
Relationship to tool-call approval
The built-in tool part approval-requested state handles the narrow case of approving a specific tool call: it renders inside the collapsible tool widget. ChatConfirmation is a broader, more prominent pattern for any "human-in-the-loop" checkpoint that does not require a structured tool invocation.
Use the stream-based tool-approval-request when:
- The approval is tied to a specific tool invocation
- You want the approval UI inline within the message
- The backend needs to resume the stream after approval
Use ChatConfirmation when:
- You need a prominent, page-level confirmation dialog
- The confirmation is not tied to a specific tool call
- You want full control over the confirmation UI and flow
Registering custom renderers for tool approval
Register a renderer that handles the approval-requested state inside tool parts:
const renderers: ChatPartRendererMap = {
tool: ({ part, message, index }) => {
const { toolInvocation } = part;
if (toolInvocation.state === 'approval-requested') {
return (
<ApprovalCard
toolName={toolInvocation.toolName}
input={toolInvocation.input}
onApprove={() =>
addToolApprovalResponse({
id: toolInvocation.approvalId ?? toolInvocation.toolCallId,
approved: true,
})
}
onDeny={(reason) =>
addToolApprovalResponse({
id: toolInvocation.approvalId ?? toolInvocation.toolCallId,
approved: false,
reason,
})
}
/>
);
}
return <ToolCard invocation={toolInvocation} />;
},
};
<ChatProvider adapter={adapter} partRenderers={renderers}>
<MyChat />
</ChatProvider>;
Accessibility
ChatConfirmationis an inline, non-modalgrouplabeled by its message. It does not steal focus or trap it, so it sits naturally in the message flow.- It is deliberately not an
alertdialog: that role promises modal focus management the card neither provides nor needs. If you require blocking confirmation, render it inside your own modal (for example a MUIDialog) and manage focus there. - The built-in inline tool widget's Approve and Deny controls are native
buttonelements, disabled while a response is pending.
For the full keyboard model and ARIA structure of the chat UI, see the Accessibility page.
See also
- Tool calling for details on the full tool invocation lifecycle and chunk protocol.
- Adapter for details on the approval response method.
- Streaming for details on the
tool-approval-requestchunk type. - Reasoning for details on displaying LLM thinking alongside tool calls.
API
See the documentation below for a complete reference to all of the props and classes available to the components mentioned here.