- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- Build your first harnessUpdated
- Host optionsNew
- Send and show mediaUpdated
- Connect clientsUpdated
- Write a client in any languageNew
- Access and tenantsNew
- Run in the terminalUpdated
- Deploy a harnessUpdated
- Send inputs safelyUpdated
- Run a tool without a model
- Durable sessionsUpdated
- Run background work on workersNew
- List, rename, and fork sessionsUpdated
- Lifecycle and retentionNew
- Run side effects onceUpdated
- Wait for a personNew
- Build on the session logUpdated
- Control how a turn endsUpdated
- Store settings per threadUpdated
- Compact a long sessionUpdated
- Fork and reset a threadUpdated
- One log for a session treeUpdated
- Write a pluginUpdated
- Track usage and costUpdated
- Ask before risky tool callsUpdated
- Give your agent coding toolsUpdated
- Sandbox and extend the coding toolsUpdated
- Run the coding tools for many threadsNew
- Undo a turnUpdated
- Build a coding agentUpdated
- Delegate to coding agentsUpdated
- Switch between agent profilesUpdated
- Auth and connectorsUpdated
- Share a thread between peopleUpdated
- Connect model providersUpdated
- Build your own UIUpdated
- Code mode in a harnessUpdated
- Work until a goal is metUpdated
- Run agents from a harnessUpdated
- Hand off a conversationNew
- Add skillsUpdated
- Use MCP serversUpdated
- Give your agent a computerNew
- Use from any MCP clientUpdated
- Use a harness from an editor (ACP)Updated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Keep Keys on the ServerNew
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Server Tools
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.
Add a server tool#
1. Define the tool#
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 };
});2. Pass the tool to chat()#
// 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);
}3. Try it#
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.
How it works#
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 client- The model sends a tool call. The arguments are a JSON string.
- chat() parses the arguments and checks them against inputSchema.
- chat() runs your .server() function with the parsed input.
- If you set an outputSchema, chat() checks the result against it. Then it adds the result to the conversation.
- The model reads the result and continues.
If the tool has needsApproval: true, the run pauses before step 3 until the user says yes. See Tool Approval Flow.
Use the signed-in user#
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);
}- If a tool sets a context type, such as .server<AppContext>(), chat() requires a context that matches it.
- A tool with no context type gets unknown context.
For middleware and values that come from the client, see Runtime Context.
Return an error the model can use#
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" };
}
});- Return { error }: you write the message that the model sees. Put the error field in outputSchema, because chat() checks the result against it.
- Throw: chat() catches the error and sends a tool error to the model. You do not control the message.
Advanced#
For input checks and repair, JSON Schema tools, progress events, long-running hosts, and file layout, see Server Tools: Advanced.