A server tool runs on your server. Use it for a database row, a secret, or an environment variable. That code never runs in the browser.
Write the function with .server(), and chat() runs it each time the model calls it.
This tool looks up an order. The Map stands in for your database:
// tools.ts
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
const orders = new Map([
["1001", { status: "shipped", eta: "2026-10-24" }],
["1002", { status: "processing", eta: "2026-10-28" }],
]);
export const getOrderStatus = toolDefinition({
name: "get_order_status",
description: "Get the shipping status of an order by its order number",
inputSchema: z.object({
orderId: z.string().meta({ description: "The order number, for example 1001" }),
}),
outputSchema: z.object({
found: z.boolean(),
status: z.string().optional(),
eta: z.string().optional(),
}),
}).server(async ({ orderId }) => {
// This runs on your server. Query your database here.
const order = orders.get(orderId);
if (!order) {
return { found: false };
}
return { found: true, ...order };
});// api/chat.ts
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { getOrderStatus } from "./tools";
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter: openaiText("gpt-6.1-sol"),
messages,
threadId,
runId,
tools: [getOrderStatus],
});
return toServerSentEventsResponse(stream);
}Ask your chat "Where is order 1001?" The model calls get_order_status and answers that the order shipped and arrives on 2026-10-24.
You did not change the client. Server tools need no client code.
sequenceDiagram
participant LLM Service
participant Server
participant Tool
participant Database/API
LLM Service->>Server: tool_call chunk<br/>{name: "get_order_status", args: {...}}
Server->>Server: Parse and check<br/>the arguments
Server->>Tool: execute(parsedArgs)
Tool->>Database/API: Query/Fetch data
Database/API-->>Tool: Return data
Tool-->>Server: Return result
Server->>Server: Create tool_result<br/>message
Server->>LLM Service: Continue chat with<br/>tool_result in history
Note over LLM Service: Model uses result<br/>to generate response
LLM Service-->>Server: Stream content chunks
Server-->>Server: Stream to clientIf the tool has needsApproval: true, the run pauses before step 3 until the user says yes. See Tool Approval Flow.
A tool often needs data from the request, such as the user ID or a database client. Pass it as context on chat(). The tool gets it as the second argument:
import { chat, toolDefinition, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { z } from "zod";
import { getSession, getDb } from "./auth";
type AppContext = {
userId: string;
db: {
users: {
findUnique(args: { where: { id: string } }): Promise<{ name: string } | null>;
};
};
};
const getCurrentUser = toolDefinition({
name: "get_current_user",
description: "Get the current authenticated user",
inputSchema: z.object({}),
outputSchema: z.object({
name: z.string().nullable(),
}),
}).server<AppContext>(async (_input, ctx) => {
const user = await ctx.context.db.users.findUnique({
where: { id: ctx.context.userId },
});
return { name: user?.name ?? null };
});
export async function POST(request: Request) {
const { messages, threadId } = await request.json();
// `getSession` and `getDb` are your own app code, not part of TanStack AI.
const session = await getSession(request);
const db = getDb();
const stream = chat({
adapter: openaiText("gpt-6.1-sol"),
messages,
threadId,
tools: [getCurrentUser],
context: {
userId: session.user.id,
db,
},
});
return toServerSentEventsResponse(stream);
}For middleware and values that come from the client, see Runtime Context.
If a lookup fails, return the error as data. The model can then tell the user or try again:
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
import { db } from "./db";
const getUserData = toolDefinition({
name: "get_user_data",
description: "Get user information",
inputSchema: z.object({
userId: z.string(),
}),
outputSchema: z.object({
name: z.string().optional(),
email: z.string().optional(),
error: z.string().optional(),
}),
}).server(async ({ userId }) => {
try {
const user = await db.users.findUnique({ where: { id: userId } });
if (!user) {
return { error: "User not found" };
}
return { name: user.name, email: user.email };
} catch {
return { error: "Failed to fetch user data" };
}
});For input checks and repair, JSON Schema tools, progress events, long-running hosts, and file layout, see Server Tools: Advanced.