Give your assistant actions like API calls, database queries, and more.
Tools enable LLMs to take actions and interact with external systems. assistant-ui provides a comprehensive toolkit for creating, managing, and visualizing tool interactions in real-time.
Overview
Tools in assistant-ui are functions that the LLM can call to perform specific tasks. They bridge the gap between the LLM's reasoning capabilities and real-world actions like:
- Fetching data from APIs
- Performing calculations
- Interacting with databases
- Controlling UI elements
- Executing workflows
When tools are executed, you can display custom generative UI components that provide rich, interactive visualizations of the tool's execution and results. Learn more in the Generative UI guide.
If you haven't provided a custom UI for a tool, assistant-ui offers a
ToolFallback component that you can add to your
codebase to render a default UI for tool executions. You can customize this by
creating your own Tool UI component for the tool's name.
Recommended: Tools() API
The Tools() API is the recommended way to register tools in assistant-ui. It provides centralized tool registration that prevents duplicate registrations and works seamlessly with all runtimes.
Quick Start
Create a toolkit object containing all your tools, then register it using useAui():
import { useAui, Tools, type Toolkit } from "@assistant-ui/react";
import { z } from "zod";
// Define your toolkit
const myToolkit: Toolkit = {
getWeather: {
description: "Get current weather for a location",
parameters: z.object({
location: z.string().describe("City name or zip code"),
unit: z.enum(["celsius", "fahrenheit"]).default("celsius"),
}),
execute: async ({ location, unit }) => {
const weather = await fetchWeatherAPI(location, unit);
return weather;
},
render: ({ args, result }) => {
if (!result) return <div>Fetching weather for {args.location}...</div>;
return (
<div className="weather-card">
<h3>{args.location}</h3>
<p>{result.temperature}° {args.unit}</p>
<p>{result.conditions}</p>
</div>
);
},
},
// Add more tools here
};
// Register tools in your runtime provider
function MyRuntimeProvider({ children }: { children: React.ReactNode }) {
const runtime = useChatRuntime();
// Register all tools
const aui = useAui({
tools: Tools({ toolkit: myToolkit }),
});
return (
<AssistantRuntimeProvider aui={aui} runtime={runtime}>
{children}
</AssistantRuntimeProvider>
);
}Benefits
- No Duplicate Registrations: Tools are registered once, preventing the "tool already exists" error
- Centralized Definition: All your tools in one place, easier to manage and test
- Type-Safe: Full TypeScript support with proper type inference
- Flexible: Works with all runtimes (AI SDK, LangGraph, custom, etc.)
- Composable: Easily split toolkits across files and merge them
Tool Definition
Each tool in the toolkit is a ToolDefinition object with these properties:
type ToolDefinition = {
description: string;
parameters: z.ZodType; // Zod schema for parameters
execute: (args, context) => Promise<any>;
render?: (props) => React.ReactNode; // Optional UI component
};Organizing Large Toolkits
For larger applications, split tools across multiple files:
// lib/tools/weather.tsx
export const weatherTools: Toolkit = {
getWeather: { /* ... */ },
getWeatherForecast: { /* ... */ },
};
// lib/tools/database.tsx
export const databaseTools: Toolkit = {
queryData: { /* ... */ },
insertData: { /* ... */ },
};
// lib/toolkit.tsx
import { weatherTools } from "./tools/weather";
import { databaseTools } from "./tools/database";
export const appToolkit: Toolkit = {
...weatherTools,
...databaseTools,
};
// App.tsx
import { appToolkit } from "./lib/toolkit";
function MyRuntimeProvider({ children }: { children: React.ReactNode }) {
const runtime = useChatRuntime();
const aui = useAui({
tools: Tools({ toolkit: appToolkit }),
});
return (
<AssistantRuntimeProvider aui={aui} runtime={runtime}>
{children}
</AssistantRuntimeProvider>
);
}UI-Only Tools
For tools where execution happens elsewhere (e.g., backend MCP tools), omit the execute function:
const uiOnlyToolkit: Toolkit = {
webSearch: {
description: "Search the web",
parameters: z.object({
query: z.string(),
}),
// No execute - handled by backend
render: ({ args, result }) => {
return (
<div>
<h3>Search: {args.query}</h3>
{result?.results.map((item) => (
<div key={item.id}>
<a href={item.url}>{item.title}</a>
</div>
))}
</div>
);
},
},
};Tool Execution Context
Tools receive additional context during execution:
execute: async (args, context) => {
// context.abortSignal - AbortSignal for cancellation
// context.toolCallId - Unique identifier for this invocation
// context.human - Function to request human input
// Example: Respect cancellation
const response = await fetch(url, { signal: context.abortSignal });
// Example: Request user confirmation
const userResponse = await context.human({
message: "Are you sure?",
});
};Human-in-the-Loop
Tools can pause execution to request user input or approval:
const confirmationToolkit: Toolkit = {
sendEmail: {
description: "Send an email with confirmation",
parameters: z.object({
to: z.string(),
subject: z.string(),
body: z.string(),
}),
execute: async ({ to, subject, body }, { human }) => {
// Request user confirmation before sending
const confirmed = await human({
type: "confirmation",
action: "send-email",
details: { to, subject },
});
if (!confirmed) {
return { status: "cancelled" };
}
await sendEmail({ to, subject, body });
return { status: "sent" };
},
render: ({ args, result, interrupt, resume }) => {
// Show confirmation dialog when waiting for user input
if (interrupt) {
return (
<div>
<h3>Confirm Email</h3>
<p>Send to: {interrupt.payload.details.to}</p>
<p>Subject: {interrupt.payload.details.subject}</p>
<button onClick={() => resume(true)}>Confirm</button>
<button onClick={() => resume(false)}>Cancel</button>
</div>
);
}
// Show result
if (result) {
return <div>Status: {result.status}</div>;
}
return <div>Preparing email...</div>;
},
},
};Alternative Methods (Legacy)
The following methods are supported for backwards compatibility but are not
recommended for new code. They can cause duplicate registration errors and are
harder to maintain. Use the Tools() API instead.
Using makeAssistantTool (Deprecated)
Register tools with the assistant context. Returns a React component that registers the tool when rendered:
import { makeAssistantTool, tool } from "@assistant-ui/react";
import { z } from "zod";
const weatherTool = tool({
description: "Get current weather for a location",
parameters: z.object({
location: z.string(),
}),
execute: async ({ location }) => {
const weather = await fetchWeatherAPI(location);
return weather;
},
});
const WeatherTool = makeAssistantTool({
...weatherTool,
toolName: "getWeather",
});
// Place inside AssistantRuntimeProvider
function App() {
return (
<AssistantRuntimeProvider runtime={runtime}>
<WeatherTool />
<Thread />
</AssistantRuntimeProvider>
);
}Why this is deprecated: Component-based registration can lead to duplicate registrations if components are remounted or if the same tool is defined in multiple places.
Using useAssistantTool Hook (Deprecated)
Register tools dynamically using React hooks:
import { useAssistantTool } from "@assistant-ui/react";
import { z } from "zod";
function DynamicTools() {
useAssistantTool({
toolName: "searchData",
description: "Search through the data",
parameters: z.object({
query: z.string(),
}),
execute: async ({ query }) => {
return await searchDatabase(query);
},
});
return null;
}Why this is deprecated: Hook-based registration ties tool definitions to component lifecycle, making them harder to test and potentially causing duplicate registrations.
Using makeAssistantToolUI (Deprecated)
Create UI-only components for tools defined elsewhere:
import { makeAssistantToolUI } from "@assistant-ui/react";
const SearchResultsUI = makeAssistantToolUI<
{ query: string },
{ results: Array<any> }
>({
toolName: "webSearch",
render: ({ args, result }) => {
return (
<div>
<h3>Search: {args.query}</h3>
{result.results.map((item) => (
<div key={item.id}>{item.title}</div>
))}
</div>
);
},
});
function App() {
return (
<AssistantRuntimeProvider runtime={runtime}>
<SearchResultsUI />
<Thread />
</AssistantRuntimeProvider>
);
}Why this is deprecated: Component-based UI registration can cause issues with tool UI not appearing or appearing multiple times.
Tool Paradigms
Frontend Tools
Tools that execute in the browser:
const frontendToolkit: Toolkit = {
screenshot: {
description: "Capture a screenshot of the current page",
parameters: z.object({
selector: z.string().optional(),
}),
execute: async ({ selector }) => {
const element = selector ? document.querySelector(selector) : document.body;
const screenshot = await captureElement(element);
return { dataUrl: screenshot };
},
},
};Backend Tools
Tools executed server-side:
// Backend route (AI SDK)
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
messages: convertToModelMessages(messages),
tools: {
queryDatabase: {
description: "Query the application database",
inputSchema: zodSchema(
z.object({
query: z.string(),
table: z.string(),
}),
),
execute: async ({ query, table }) => {
const results = await db.query(query, { table });
return results;
},
},
},
});
return result.toUIMessageStreamResponse();
}Client-Defined Tools with frontendTools
The Vercel AI SDK adapter implements automatic serialization of client-defined tools. Tools registered via the Tools() API are automatically included in API requests:
// Frontend: Define tools with Tools() API
const clientToolkit: Toolkit = {
calculate: {
description: "Perform calculations",
parameters: z.object({
expression: z.string(),
}),
execute: async ({ expression }) => {
return eval(expression); // Use proper parser in production
},
},
};
function MyRuntimeProvider({ children }: { children: React.ReactNode }) {
const runtime = useChatRuntime();
const aui = useAui({
tools: Tools({ toolkit: clientToolkit }),
});
return (
<AssistantRuntimeProvider aui={aui} runtime={runtime}>
{children}
</AssistantRuntimeProvider>
);
}
// Backend: Use frontendTools to receive client tools
import { frontendTools } from "@assistant-ui/react-ai-sdk";
export async function POST(req: Request) {
const { messages, tools } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
messages: convertToModelMessages(messages),
tools: {
...frontendTools(tools), // Client-defined tools
// Additional server-side tools
queryDatabase: {
description: "Query the database",
inputSchema: zodSchema(z.object({ query: z.string() })),
execute: async ({ query }) => {
return await db.query(query);
},
},
},
});
return result.toUIMessageStreamResponse();
}MCP (Model Context Protocol) Tools
Integration with MCP servers using AI SDK's experimental MCP support:
import { experimental_createMCPClient, streamText } from "ai";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
export async function POST(req: Request) {
const client = await experimental_createMCPClient({
transport: new StdioClientTransport({
command: "npx",
args: ["@modelcontextprotocol/server-github"],
}),
});
try {
const tools = await client.tools();
const result = streamText({
model: openai("gpt-4o"),
tools,
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
} finally {
await client.close();
}
}Best Practices
- Use Tools() API: Always prefer the
Tools()API over legacy component/hook-based registration - Centralize Definitions: Keep all tools in a toolkit file for easy management
- Clear Descriptions: Write descriptive tool descriptions that help the LLM understand when to use each tool
- Parameter Validation: Use Zod schemas to ensure type safety
- Error Handling: Handle errors gracefully with user-friendly messages
- Loading States: Provide visual feedback during tool execution
- Security: Validate permissions and sanitize inputs
- Performance: Use abort signals for cancellable operations
- Testing: Test tools in isolation and with the full assistant flow
Migration from Legacy APIs
To migrate from legacy APIs to the Tools() API:
- Create a toolkit object with all your tools
- Move tool definitions from
makeAssistantTool/useAssistantToolcalls into the toolkit - Register once using
useAui({ tools: Tools({ toolkit }) })in your runtime provider - Remove component registrations (
<WeatherTool />, etc.) - Test to ensure all tools work as expected
Example migration:
// Before (Legacy)
const WeatherTool = makeAssistantTool({
toolName: "getWeather",
description: "Get weather",
parameters: z.object({ location: z.string() }),
execute: async ({ location }) => { /* ... */ },
});
function App() {
return (
<AssistantRuntimeProvider runtime={runtime}>
<WeatherTool />
<Thread />
</AssistantRuntimeProvider>
);
}
// After (Recommended)
const toolkit: Toolkit = {
getWeather: {
description: "Get weather",
parameters: z.object({ location: z.string() }),
execute: async ({ location }) => { /* ... */ },
},
};
function MyRuntimeProvider({ children }: { children: React.ReactNode }) {
const runtime = useChatRuntime();
const aui = useAui({
tools: Tools({ toolkit }),
});
return (
<AssistantRuntimeProvider aui={aui} runtime={runtime}>
{children}
</AssistantRuntimeProvider>
);
}