Give HeroUI Agent typed access to browser data and actions with createToolHelper.
Client tools execute in the user's browser with the user's current session. Only each tool's name, description, JSON schema, and approval flag are sent to the hosted Agent; implementation code and shared context stay on the page.
They are one of three kinds of tool an agent can use. See Tools for how client tools compare to built-in toolkits and MCP servers, and which to reach for.
createToolHelper<Context>() returns a tool factory bound to your application context type. Zod parameters provide typed arguments and runtime parsing.
Import z from @heroui/agent/zod to use the SDK’s pinned version. No separate Zod installation
is needed. Raw JSON Schema is also supported.
import {HeroUIAgent, createToolHelper} from "@heroui/agent";
import {z} from "@heroui/agent/zod";
type AppContext = {
apiClient: ApiClient;
page: () => {route: string};
};
const tool = createToolHelper<AppContext>();
const tools = [
tool({
name: "search_users",
displayName: "Search users",
description: "Search users by name or email",
parameters: z.object({query: z.string()}),
execute: ({query}, context) => context.apiClient.searchUsers(query),
}),
];
<HeroUIAgent
getAuthToken={getAuthToken}
context={{apiClient, page: () => ({route: location.pathname})}}
agentId={process.env.HEROUI_AGENT_ID!}
tools={tools}
/>;The CDN loader accepts the same tool shape with raw JSON Schema instead of Zod. Implementations and context still run in the customer page:
<script src="https://agent.heroui.pro/loader.js" defer></script>
<script>
window.addEventListener("heroui-agent:loaded", () => {
window.agent = window.HeroUIAgent.mount({
agentId: "your_agent_id",
getAuthToken,
context: {
apiClient,
page: () => ({route: location.pathname}),
},
tools: [
{
name: "search_users",
displayName: "Search users",
description: "Search users by name or email",
parameters: {
type: "object",
properties: {query: {type: "string"}},
required: ["query"],
},
execute: ({query}, context) => context.apiClient.searchUsers(query),
},
],
});
});
</script>| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Stable machine-readable tool name. |
displayName | string | No | Human-readable name shown in the default tool card. |
description | string | Yes | Clear instruction that helps the model decide when to call the tool. |
parameters | ZodType | Record<string, unknown> | Yes | Zod schema or raw JSON Schema for tool arguments. |
execute | (args, context, execution) => unknown | Promise<unknown> | Yes | Browser implementation. Returned data can feed generated UI and files. |
needsApproval | boolean | No | Require user approval in the default auto permission mode. |
icon | ClientToolIcon | No | Hosted icon identifier: add, database, delete, edit, navigate, search, sparkles, or view. |
iconColor | string | No | Color for the hosted tool icon. |
The hosted iframe renders approval requests and action failures. Successful direct toolCall
actions from generated Buttons and ActionGroups execute silently, without adding a completion label
or JSON result below the button. Model-initiated tools can still appear in the activity trail.
Tool results must be JSON-serializable; return null or undefined when the action has no data to
return. Completion is still recorded for replay protection. Arbitrary React render callbacks cannot cross
the iframe boundary. A custom component API for tool states is coming soon; see
Custom Components for what is available today.
Every tool receives the same context object. It may contain API clients, authenticated user details, state setters, and other browser-only values. Those values never enter the iframe. The optional page() key is reserved: the bridge calls it on demand and sends its JSON-serializable return value as page context, limited to 16 KB.
Mark destructive or consequential actions with needsApproval: true. Permission modes only govern
declared client tools; they do not grant additional account, filesystem, or network access.
Normal client-tool results are limited to 64 KiB. That is enough for requests such as “show the latest 100 conversations,” and built-in file generation can export those exact rows on a later turn.
For larger JSON, CSV, or TSV exports, use the third execution argument. uploadDataSource stores up to 10 MB in the current conversation and returns a small AgentDataSource handle:
tool({
name: "export_conversations",
description: "Load conversations for analysis or export",
parameters: z.object({limit: z.number().int().max(10_000)}),
execute: async ({limit}, context, execution) => {
const conversations = await context.apiClient.listConversations({limit});
const dataSource = await execution.uploadDataSource({
data: conversations,
filename: "conversations.json",
format: "json",
});
return {count: conversations.length, dataSource};
},
});Handles are private to the current agent and conversation. The hosted runtime validates ownership before streaming the source into its offline generator; returning a URL or forged handle does not bypass that check.
HeroUIAgent and HeroUIAgentProps<TContext> accept both interfaces and type aliases without an index signature. Use the same context type in your tool helper:
interface DashboardContext {
serverId: string;
refresh: () => void;
}
const tool = createToolHelper<DashboardContext>();
const context: DashboardContext = {serverId: "east", refresh};
<HeroUIAgent agentId={agentId} getAuthToken={getAuthToken} context={context} tools={tools} />;Context stays in your page and is available to tool execution. The reserved page property, when provided, must be a callback returning page context (or a promise for it). Only its explicit result crosses the iframe bridge; arbitrary context fields and callbacks do not.
Import z from @heroui/agent/zod to use the SDK's pinned schema version. This separate entry keeps Zod out of lightweight root imports; no separate Zod installation is needed.
Throw an Error with a useful message when execution fails. The Agent receives that message (up to 1,000 characters), and Monitor → Logs → Tool errors records it with the tool name and call ID. Error stacks and arbitrary exception fields are not forwarded. Keep credentials and other secrets out of error messages.
| Code | Meaning | Recovery |
|---|---|---|
CLIENT_TOOL_EXECUTION_ERROR | The tool threw an error. | Use the error message to diagnose the failure. |
CLIENT_TOOL_RESULT_TOO_LARGE | The serialized result exceeds 64 KiB. | Return fewer fields or rows, paginate, or use execution.uploadDataSource for a larger dataset. |
CLIENT_TOOL_RESULT_NOT_SERIALIZABLE | The result contains a circular reference or a non-JSON value. | Return plain objects and arrays; explicitly convert Map and Date values. |
An exception or invalid result does not prove that an action had no effect. The runtime persists the error as an interrupted receipt and replays it after reconnecting; it does not repeat the tool execution. Check the current application state before attempting the action again.
Reserved names such as searchWeb, composeUI, and names beginning with mcp_ are rejected at mount with the tool name in the error. Rename the client tool. Configuration failures are reported once; fix the props to retry. The iframe handshake uses bounded backoff when the host is unavailable.