Tools

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.

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

  1. Use Tools() API: Always prefer the Tools() API over legacy component/hook-based registration
  2. Centralize Definitions: Keep all tools in a toolkit file for easy management
  3. Clear Descriptions: Write descriptive tool descriptions that help the LLM understand when to use each tool
  4. Parameter Validation: Use Zod schemas to ensure type safety
  5. Error Handling: Handle errors gracefully with user-friendly messages
  6. Loading States: Provide visual feedback during tool execution
  7. Security: Validate permissions and sanitize inputs
  8. Performance: Use abort signals for cancellable operations
  9. Testing: Test tools in isolation and with the full assistant flow

Migration from Legacy APIs

To migrate from legacy APIs to the Tools() API:

  1. Create a toolkit object with all your tools
  2. Move tool definitions from makeAssistantTool/useAssistantTool calls into the toolkit
  3. Register once using useAui({ tools: Tools({ toolkit }) }) in your runtime provider
  4. Remove component registrations (<WeatherTool />, etc.)
  5. 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>
  );
}