Chat - Tool Approval
Add human-in-the-loop checkpoints before the agent executes tool calls, using the approval lifecycle and the ChatConfirmation UI component.
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 additional 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().
Stream chunk: tool-approval-request
When the backend determines a tool call needs user approval, it sends a tool-approval-request chunk:
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 |
object |
The tool's input, shown to the user |
approvalId |
string | undefined |
Optional distinct approval ID |
When this chunk arrives, the tool invocation moves to state: 'approval-requested'.
The addToolApprovalResponse adapter method
Implement addToolApprovalResponse on your adapter to send the user's decision to the backend:
interface ChatAddToolApproveResponseInput {
id: string; // the approval request ID from the stream chunk
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 }),
});
},
Approve/deny flow in the UI
Use useChat() to call addToolApprovalResponse from your component:
const { addToolApprovalResponse } = useChat();
// Approve
await addToolApprovalResponse({
id: toolCall.toolCallId,
approved: true,
});
// Deny with reason
await addToolApprovalResponse({
id: 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.
ChatConfirmation UI component
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()}
/>;
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.toolCallId,
approved: true,
})
}
onDeny={(reason) =>
addToolApprovalResponse({
id: toolInvocation.toolCallId,
approved: false,
reason,
})
}
/>
);
}
return <ToolCard invocation={toolInvocation} />;
},
};
<ChatProvider adapter={adapter} partRenderers={renderers}>
<MyChat />
</ChatProvider>;
See also
- Tool Calling for the full tool invocation lifecycle and chunk protocol.
- Adapter for the
addToolApprovalResponse()method reference. - Streaming for the
tool-approval-requestchunk type. - Reasoning for 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.