Tools
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.
Tool Creation Methods
assistant-ui offers multiple ways to create and register tools, each suited for different use cases:
makeAssistantTool
: Register client-defined tools with the assistant contextuseAssistantTool
: Hook-based dynamic tool registrationmakeAssistantToolUI
: UI-only components for existing tools- Inline registration: Direct tool registration with the runtime
1. Using makeAssistantTool
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";
// Define the tool
const weatherTool = tool({
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 }) => {
// Tool execution logic
const weather = await fetchWeatherAPI(location, unit);
return weather;
}
});
// Create the component
const WeatherTool = makeAssistantTool(weatherTool);
// Place the tool component inside AssistantRuntimeProvider
function App() {
return (
<AssistantRuntimeProvider runtime={runtime}>
<WeatherTool />
<Thread />
</AssistantRuntimeProvider>
);
}
When using server-side runtimes like Vercel AI SDK, you can pass client-defined tools to your backend using frontendTools
. See the Client-Defined Tools with frontendTools section below.
2. Using useAssistantTool
Hook
Register tools dynamically using React hooks. Useful for conditional tools or when tool availability depends on component state:
import { useAssistantTool } from "@assistant-ui/react";
import { z } from "zod";
function DynamicTools() {
const [dataSource, setDataSource] = useState<"local" | "cloud">("local");
useAssistantTool({
toolName: "searchData",
description: "Search through the selected data source",
parameters: z.object({
query: z.string()
}),
execute: async ({ query }) => {
if (dataSource === "local") {
return await searchLocalDatabase(query);
} else {
return await searchCloudDatabase(query);
}
},
// Re-register when data source changes
enabled: true
});
return null;
}
3. Using makeAssistantToolUI
Create generative UI components for tools that are defined elsewhere. This is UI-only - the tool's execution logic must be registered separately (e.g., in your backend, MCP server, or another component):
This creates only the UI component. The actual tool execution happens where you've defined it (typically in your API route with server-based runtimes like Vercel AI SDK).
import { makeAssistantToolUI, AssistantToolUI } from "@assistant-ui/react";
const SearchResultsUI = makeAssistantToolUI<{
query: string;
}, {
results: Array<{
id: string;
url: string;
title: string;
snippet: string;
}>;
}>({
toolName: "webSearch", // Must match the registered tool's name
render: ({ args, result }) => {
return (
<div className="search-results">
<h3>Search: {args.query}</h3>
{result.results.map((item) => (
<div key={item.id}>
<a href={item.url}>{item.title}</a>
<p>{item.snippet}</p>
</div>
))}
</div>
);
}
});
// Place the tool component inside AssistantRuntimeProvider
function App() {
return (
<AssistantRuntimeProvider runtime={runtime}>
<SearchResultsUI />
<Thread />
</AssistantRuntimeProvider>
);
}
4. Using Inline Tool Registration
For simple tools without custom UI:
import { tool } from "@assistant-ui/react";
const calculateTool = tool({
description: "Perform mathematical calculations",
parameters: z.object({
expression: z.string()
}),
execute: async ({ expression }) => {
return eval(expression); // Note: Use proper math parser in production
}
});
// Register with runtime
runtime.registerTool(calculateTool);
Tool Paradigms
Frontend Tools
Tools that execute in the browser, accessing client-side resources:
const screenshotTool = tool({
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 };
}
});
const ScreenshotTool = makeAssistantTool(screenshotTool);
Backend Tools
Tools that trigger server-side operations:
// Backend route (AI SDK)
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
messages,
tools: {
queryDatabase: {
description: "Query the application database",
parameters: z.object({
query: z.string(),
table: z.string()
}),
execute: async ({ query, table }) => {
// Server-side database access
const results = await db.query(query, { table });
return results;
}
}
}
});
return result.toDataStreamResponse();
}
Client-Defined Tools with frontendTools
When using the Vercel AI SDK, you can define tools on the frontend and pass them to your backend using the frontendTools
utility:
// Frontend: Define tool with makeAssistantTool
import { makeAssistantTool, tool } from "@assistant-ui/react";
const calculateTool = tool({
description: "Perform calculations",
parameters: z.object({
expression: z.string()
}),
execute: async ({ expression }) => {
return eval(expression); // Note: Use proper math parser in production
}
});
const CalculateTool = makeAssistantTool(calculateTool);
// 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,
tools: {
...frontendTools(tools), // Client-defined tools
// Additional server-side tools
queryDatabase: {
description: "Query the application database",
parameters: z.object({ query: z.string() }),
execute: async ({ query }) => {
return await db.query(query);
}
}
}
});
return result.toDataStreamResponse();
}
The frontendTools
utility is currently only available for the Vercel AI SDK integration. Tools defined on the frontend are serialized and executed on the server for security.
Human-in-the-Loop Tools
Tools that require human approval or input:
import { makeAssistantTool, tool } from "@assistant-ui/react";
import { z } from "zod";
const refundTool = tool({
description: "Process a customer refund",
parameters: z.object({
orderId: z.string(),
amount: z.number(),
reason: z.string()
}),
execute: async ({ orderId, amount, reason }) => {
// Wait for human approval
const approved = await requestHumanApproval({
action: "refund",
details: { orderId, amount, reason }
});
if (!approved) {
throw new Error("Refund rejected by administrator");
}
return await processRefund(orderId, amount);
}
});
const RefundTool = makeAssistantTool(refundTool);
MCP (Model Context Protocol) Tools
Integration with MCP servers:
// Using AI SDK's MCP support
import { createMCPClient } from "ai/mcp";
const mcpClient = createMCPClient({
servers: {
github: {
command: "npx",
args: ["@modelcontextprotocol/server-github"]
}
}
});
// Tools are automatically available through the runtime
const runtime = useChatRuntime({
api: "/api/chat",
tools: await mcpClient.getTools()
});
Advanced Patterns
Tool Composition
Combining multiple tools for complex workflows:
const travelPlannerTool = tool({
description: "Plan a complete trip itinerary",
parameters: z.object({
destination: z.string(),
dates: z.object({
start: z.string(),
end: z.string()
})
}),
execute: async ({ destination, dates }) => {
// Execute multiple operations
const weather = await getWeatherAPI(destination);
const hotels = await searchHotelsAPI({
location: destination,
dates
});
const activities = await findActivitiesAPI({
location: destination,
weather: weather.forecast
});
return {
weather,
hotels,
activities,
itinerary: generateItinerary({ weather, hotels, activities })
};
}
});
const TravelPlannerTool = makeAssistantTool(travelPlannerTool);
Conditional Tool Availability
Tools that appear based on context:
function ConditionalTools() {
const { user } = useAuth();
const { subscription } = useSubscription();
// Premium features
useAssistantTool({
toolName: "advancedAnalysis",
description: "Perform advanced data analysis",
parameters: z.object({
dataset: z.string()
}),
execute: async (args) => {
// Premium analysis logic
},
enabled: subscription?.tier === "premium"
});
// Role-based tools
useAssistantTool({
toolName: "adminPanel",
description: "Access admin controls",
parameters: z.object({}),
execute: async () => {
// Admin actions
},
enabled: user?.role === "admin"
});
}
Tool Error Handling
Robust error handling and recovery:
const resilientTool = tool({
description: "Fetch data with retry logic",
parameters: z.object({
endpoint: z.string()
}),
execute: async ({ endpoint }, { abortSignal }) => {
const maxRetries = 3;
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(endpoint, { signal: abortSignal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
lastError = error;
if (abortSignal.aborted) throw error; // Don't retry on abort
await new Promise(resolve => setTimeout(resolve, 1000 * i));
}
}
throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}
});
const ResilientTool = makeAssistantTool(resilientTool);
Best Practices
- 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 and provide clear parameter descriptions
- Error Handling: Always handle potential errors gracefully with user-friendly messages
- Loading States: Provide visual feedback during tool execution
- Security: Validate permissions and sanitize inputs, especially for destructive operations
- Performance: Use abort signals for cancellable operations and implement timeouts
- Testing: Test tools in isolation and with the full assistant flow
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
}
Runtime Integration
Each runtime handles tools differently:
Server-Based Runtimes
- AI SDK: Tools defined in API routes with
streamText({ tools: {...} })
- execution happens server-side - LangGraph: Tools defined in your LangGraph graph configuration
- Mastra: Tools defined as typed functions used by agents and workflows
Client-Based Runtimes
- LocalRuntime: Tools passed via context or registered with
makeAssistantTool
- External Store: Tools registered on the runtime with
runtime.registerTool()
All runtimes support the same frontend tool UI components - just ensure the UI components are rendered inside <AssistantRuntimeProvider>
. See the runtime documentation for specific examples.