Skip to main content

Custom Tools (React)

Tools let the agent call functions in the user's browser. When the agent decides to use a tool, the SDK executes it client-side and sends the result back to continue the conversation.

Defining tools

Use createToolHelper for type-safe tool definitions with Zod schema inference:

import { createToolHelper } from '@kapaai/agent-react';
import { z } from 'zod';

type MyContext = { apiClient: ApiClient };
const tool = createToolHelper<MyContext>();

const myTools = [
tool({
name: 'search_users',
description: 'Search for users by name or email',
parameters: z.object({
query: z.string().describe('Search query'),
limit: z.number().optional().describe('Max results'),
}),
displayName: 'Search Users',
execute: async ({ query, limit }, ctx) => {
const results = await ctx.apiClient.searchUsers(query, limit);
return results;
},
}),
];

The execute function receives:

  • args — parsed and typed from the Zod schema (e.g. { query: string, limit?: number })
  • context — the context object you passed to AgentProvider

Tool options

FieldTypeRequiredDescription
namestringYesUnique tool name (sent to the LLM).
descriptionstringYesDescription of what the tool does (sent to the LLM).
parametersZodType | objectYesZod schema or raw JSON Schema for the tool's parameters.
execute(args, context) => Promise<unknown>YesFunction to execute when the tool is called.
displayNamestringNoHuman-readable name shown in the UI. Defaults to name.
needsApprovalbooleanNoIf true, the user must approve before execution.
iconComponentTypeNoIcon component shown in the tool card.
iconColorstringNoColor for the icon.
render(props) => ReactNodeNoCustom rendering for the tool card. See Custom rendering.

The icon component should accept { size?: number | string; color?: string; stroke?: number | string } props. Any icon library that follows this pattern works (e.g. Tabler Icons, Lucide, Heroicons).

Approval flow

Set needsApproval: true to require user confirmation before a tool executes:

tool({
name: 'delete_record',
description: 'Delete a database record',
parameters: z.object({ id: z.string() }),
needsApproval: true,
execute: async ({ id }, ctx) => ctx.db.delete(id),
}),

When the agent calls this tool, the SDK shows the tool card in an expanded state with Allow and Deny buttons. The tool only executes after the user clicks Allow.

Custom rendering

The render prop lets you replace the default tool card with custom JSX:

tool({
name: 'get_weather',
description: 'Get weather for a city',
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => fetchWeather(city),
render: ({ status, args, result }) => {
if (status === 'executing') return <div>Checking weather for {args.city}...</div>;
if (status === 'completed') {
const data = result as WeatherData;
return <WeatherCard data={data} />;
}
return null; // Fall back to default ToolCallCard
},
}),

Render props

FieldTypeDescription
statusToolCallStatusCurrent tool execution status. See ToolCallStatus.
argsInferred from Zod schemaThe arguments the agent passed to the tool.
resultunknownThe return value of execute (only when status === 'completed').
errorstringError message (only when status === 'error').
onApprove() => voidCall to approve the tool (only when status === 'approval_requested').
onReject() => voidCall to reject the tool (only when status === 'approval_requested').

Fallback behavior

If your render function returns null for a given status, the SDK falls back to the default ToolCallCard. This means you can customize only the states you care about:

render: ({ status, result }) => {
// Only customize the completed state
if (status === 'completed') return <MyCustomResult data={result} />;
// Everything else (approval, executing, error) uses the default card
return null;
},

This is especially useful with needsApproval — you can let the SDK handle the approval UI and only customize the result display.

Context

The context object is shared across all tools and can hold anything — API clients, auth tokens, user info:

const context = useMemo(
() => ({
apiClient: new ApiClient(authToken),
userId: currentUser.id,
}),
[authToken, currentUser.id],
);

<AgentProvider tools={myTools} context={context} {...otherProps}>

Tools access it as the second argument to execute:

execute: async (args, ctx) => {
return ctx.apiClient.query(args.sql);
},

Without Zod

If you don't want to use Zod, pass a raw JSON Schema object as parameters:

tool({
name: 'search',
description: 'Search for items',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
},
required: ['query'],
},
execute: async (args) => {
return searchItems(args.query);
},
}),
note

Without Zod, the execute and render args are typed as Record<string, unknown> — you'll need to cast or validate manually.

Built-in tools

The agent has one built-in tool: search_knowledge_base. This tool searches your project's knowledge base — the same sources you've configured in the Kapa dashboard (documentation, GitHub repos, API references, etc.). It runs entirely server-side using Kapa's retrieval pipeline, so you don't define it as a client tool and can't customize its execution.

What you can customize is how the tool appears in the chat UI. Use builtinToolMeta to set a display name and optional icon:

<AgentProvider
builtinToolMeta={{
search_knowledge_base: {
displayName: 'Search Knowledge Base',
icon: IconSearch, // optional — any component with { size?, color?, stroke? } props
iconColor: '#38bdf8', // optional
},
}}
{...otherProps}
>

Without builtinToolMeta, the tool card will show the raw name search_knowledge_base in the chat. With it, users see "Search Knowledge Base" instead.