Tools (also called "function calling") allow AI models to interact with external systems, APIs, or perform computations. TanStack AI provides an isomorphic tool system that enables type-safe, framework-agnostic tool definitions that work on both server and client.
Tools enable your AI application to:
TanStack AI works with any JavaScript framework:
TanStack AI works with any JavaScript framework.
TanStack AI uses a two-step tool definition process:
This approach provides:
Tools are defined using toolDefinition() from @tanstack/ai:
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
// Step 1: Define the tool schema
const getWeatherDef = toolDefinition({
name: "get_weather",
description: "Get the current weather for a location",
inputSchema: z.object({
location: z.string().describe("The city and state, e.g. San Francisco, CA"),
unit: z.enum(["celsius", "fahrenheit"]).optional(),
}),
outputSchema: z.object({
temperature: z.number(),
conditions: z.string(),
location: z.string(),
}),
});
// Step 2: Create a server implementation
const getWeatherServer = getWeatherDef.server(async ({ location, unit }) => {
const response = await fetch(
`https://api.weather.com/v1/current?location=${location}&unit=${
unit || "fahrenheit"
}`
);
const data = await response.json();
return {
temperature: data.temperature,
conditions: data.conditions,
location: data.location,
};
});
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
// Step 1: Define the tool schema
const getWeatherDef = toolDefinition({
name: "get_weather",
description: "Get the current weather for a location",
inputSchema: z.object({
location: z.string().describe("The city and state, e.g. San Francisco, CA"),
unit: z.enum(["celsius", "fahrenheit"]).optional(),
}),
outputSchema: z.object({
temperature: z.number(),
conditions: z.string(),
location: z.string(),
}),
});
// Step 2: Create a server implementation
const getWeatherServer = getWeatherDef.server(async ({ location, unit }) => {
const response = await fetch(
`https://api.weather.com/v1/current?location=${location}&unit=${
unit || "fahrenheit"
}`
);
const data = await response.json();
return {
temperature: data.temperature,
conditions: data.conditions,
location: data.location,
};
});
import { chat, toStreamResponse } from "@tanstack/ai";
import { openai } from "@tanstack/ai-openai";
import { getWeatherDef } from "./tools";
export async function POST(request: Request) {
const { messages } = await request.json();
// Create server implementation
const getWeather = getWeatherDef.server(async ({ location, unit }) => {
const response = await fetch(`https://api.weather.com/v1/current?...`);
return await response.json();
});
const stream = chat({
adapter: openai(),
messages,
model: "gpt-4o",
tools: [getWeather], // Pass server tools
});
return toStreamResponse(stream);
}
import { chat, toStreamResponse } from "@tanstack/ai";
import { openai } from "@tanstack/ai-openai";
import { getWeatherDef } from "./tools";
export async function POST(request: Request) {
const { messages } = await request.json();
// Create server implementation
const getWeather = getWeatherDef.server(async ({ location, unit }) => {
const response = await fetch(`https://api.weather.com/v1/current?...`);
return await response.json();
});
const stream = chat({
adapter: openai(),
messages,
model: "gpt-4o",
tools: [getWeather], // Pass server tools
});
return toStreamResponse(stream);
}
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import {
clientTools,
createChatClientOptions,
type InferChatMessages
} from "@tanstack/ai-client";
import { updateUIDef, saveToStorageDef } from "./tools";
// Create client implementations
const updateUI = updateUIDef.client((input) => {
// Update UI state
setNotification(input.message);
return { success: true };
});
const saveToStorage = saveToStorageDef.client((input) => {
localStorage.setItem("data", JSON.stringify(input));
return { saved: true };
});
// Create typed tools array (no 'as const' needed!)
const tools = clientTools(updateUI, saveToStorage);
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
// Infer message types for full type safety
type ChatMessages = InferChatMessages<typeof chatOptions>;
function ChatComponent() {
const { messages, sendMessage } = useChat(chatOptions);
// messages is now fully typed with tool names and outputs!
return <Messages messages={messages} />;
}
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import {
clientTools,
createChatClientOptions,
type InferChatMessages
} from "@tanstack/ai-client";
import { updateUIDef, saveToStorageDef } from "./tools";
// Create client implementations
const updateUI = updateUIDef.client((input) => {
// Update UI state
setNotification(input.message);
return { success: true };
});
const saveToStorage = saveToStorageDef.client((input) => {
localStorage.setItem("data", JSON.stringify(input));
return { saved: true };
});
// Create typed tools array (no 'as const' needed!)
const tools = clientTools(updateUI, saveToStorage);
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
// Infer message types for full type safety
type ChatMessages = InferChatMessages<typeof chatOptions>;
function ChatComponent() {
const { messages, sendMessage } = useChat(chatOptions);
// messages is now fully typed with tool names and outputs!
return <Messages messages={messages} />;
}
Tools can be implemented for both server and client, enabling flexible execution patterns:
// Define once
const addToCartDef = toolDefinition({
name: "add_to_cart",
description: "Add item to shopping cart",
inputSchema: z.object({
itemId: z.string(),
quantity: z.number(),
}),
outputSchema: z.object({
success: z.boolean(),
cartId: z.string(),
}),
needsApproval: true,
});
// Server implementation - Store in database
const addToCartServer = addToCartDef.server(async (input) => {
const cart = await db.carts.create({
data: { itemId: input.itemId, quantity: input.quantity },
});
return { success: true, cartId: cart.id };
});
// Client implementation - Update local wishlist
const addToCartClient = addToCartDef.client((input) => {
const wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
wishlist.push(input.itemId);
localStorage.setItem("wishlist", JSON.stringify(wishlist));
return { success: true, cartId: "local" };
});
// Define once
const addToCartDef = toolDefinition({
name: "add_to_cart",
description: "Add item to shopping cart",
inputSchema: z.object({
itemId: z.string(),
quantity: z.number(),
}),
outputSchema: z.object({
success: z.boolean(),
cartId: z.string(),
}),
needsApproval: true,
});
// Server implementation - Store in database
const addToCartServer = addToCartDef.server(async (input) => {
const cart = await db.carts.create({
data: { itemId: input.itemId, quantity: input.quantity },
});
return { success: true, cartId: cart.id };
});
// Client implementation - Update local wishlist
const addToCartClient = addToCartDef.client((input) => {
const wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
wishlist.push(input.itemId);
localStorage.setItem("wishlist", JSON.stringify(wishlist));
return { success: true, cartId: "local" };
});
On the server, pass the definition (for client execution) or server implementation:
chat({
adapter: openai(),
messages,
tools: [addToCartDef], // Client will execute, or
tools: [addToCartServer], // Server will execute
});
chat({
adapter: openai(),
messages,
tools: [addToCartDef], // Client will execute, or
tools: [addToCartServer], // Server will execute
});
The isomorphic architecture provides complete type safety:
// In your React component
messages.forEach((message) => {
message.parts.forEach((part) => {
if (part.type === 'tool-call' && part.name === 'add_to_cart') {
// ✅ TypeScript knows part.name is literally 'add_to_cart'
// ✅ part.input is typed as { itemId: string, quantity: number }
// ✅ part.output is typed as { success: boolean, cartId: string } | undefined
if (part.output) {
console.log(part.output.cartId); // ✅ Fully typed!
}
}
});
});
// In your React component
messages.forEach((message) => {
message.parts.forEach((part) => {
if (part.type === 'tool-call' && part.name === 'add_to_cart') {
// ✅ TypeScript knows part.name is literally 'add_to_cart'
// ✅ part.input is typed as { itemId: string, quantity: number }
// ✅ part.output is typed as { success: boolean, cartId: string } | undefined
if (part.output) {
console.log(part.output.cartId); // ✅ Fully typed!
}
}
});
});
Tools go through different states during execution:
