- 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
- 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
- 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
Runtime Context
runtimeContext passes a user id, tenant, or database client into a tool. Those values stay out of the prompt.
Pass them as runtime context. Your tools and middleware read them from ctx.context, with full types. The model never sees them.
Quick setup#
1. Declare the context that the tool needs#
Put the context type on .server<TContext>(). The tool reads the values from ctx.context:
// lib/ai/tools.ts
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
export type AppContext = {
userId: string
}
const notesByUser = new Map([['user_123', [{ title: 'Groceries' }]]])
export const listNotes = toolDefinition({
name: 'list_notes',
description: 'List notes for the current user',
inputSchema: z.object({}),
outputSchema: z.array(z.object({ title: z.string() })),
}).server<AppContext>(async (_input, ctx) => {
return notesByUser.get(ctx.context.userId) ?? []
})2. Pass the context to chat()#
// api/chat.ts
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { listNotes } from '../lib/ai/tools'
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request)
// Replace this line with your auth check.
const userId = request.headers.get('x-user-id') ?? 'user_123'
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages,
threadId,
runId,
tools: [listNotes],
context: { userId },
})
return toServerSentEventsResponse(stream)
}Ask "What are my notes?". The tool answers with the notes of the current user. If you remove context, TypeScript reports an error, because listNotes needs userId.
How it works#
The tools and middleware are the contract. The context value is what you supply at runtime.
- Each tool or middleware declares the context shape that it needs.
- chat() merges these needs and checks your context value against the result.
- The value goes only to your tools and middleware. It is not sent to the model.
- It is also not the AG-UI RunAgentInput.context field. See AG-UI context.
Declare the type in one of these places:
- toolDefinition(...).server<TContext>(...) for server tools.
- toolDefinition(...).client<TContext>(...) for client tools.
- ChatMiddleware<TContext> for middleware.
Server Runtime Context#
Middleware reads the same context. Give it the type with ChatMiddleware<TContext>:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import type { ChatMiddleware } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
type AppContext = {
userId: string
tenantId: string
}
const auditMiddleware: ChatMiddleware<AppContext> = {
name: 'audit',
onStart(ctx) {
console.log('chat started', {
requestId: ctx.requestId,
userId: ctx.context.userId,
tenantId: ctx.context.tenantId,
})
},
}
export async function POST(request: Request) {
const { messages, threadId } = await chatParamsFromRequest(request)
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages,
threadId,
middleware: [auditMiddleware],
context: {
userId: 'user_123',
tenantId: 'tenant_456',
},
})
return toServerSentEventsResponse(stream)
}You can put any value in the context, for example a database client or an audit logger.
Client Runtime Context#
Client tools can have runtime context too. Pass it to useChat. It stays in the browser and is not sent to the server.
import { toolDefinition } from '@tanstack/ai'
import { createChatClientOptions } from '@tanstack/ai-client'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
type ClientContext = {
currentTabId: string
toast(message: string): void
}
const notifyUser = toolDefinition({
name: 'notify_user',
description: 'Show a notification in the current browser tab',
}).client<ClientContext>((_input, ctx) => {
ctx.context.toast(`Updated tab ${ctx.context.currentTabId}`)
return { ok: true }
})
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
tools: [notifyUser],
context: {
currentTabId: 'settings',
toast: (message) => window.alert(message),
},
})
export function Chat() {
const { sendMessage } = useChat(chatOptions)
return <button onClick={() => sendMessage('Update my tab')}>Send</button>
}Use client context for local values only. The server does not receive them.
Client-to-Server Handoff#
To send a value from the client to the server, use forwardedProps. On the server, validate it, then copy it into the server context.
On the client, put the value in forwardedProps. Unlike context, it goes to the server:
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
export function Chat() {
const { sendMessage } = useChat({
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
forwardedProps: {
tenantId: 'tenant_456',
},
})
return <button onClick={() => sendMessage('Hello')}>Send</button>
}On the server, read forwardedProps and build the context:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
type AppContext = {
userId: string
tenantId: string
}
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
// Replace these lines with your auth check and your allowlist of tenants.
const userId = 'user_123'
const defaultTenantId = 'tenant_default'
const tenantId =
typeof params.forwardedProps.tenantId === 'string'
? params.forwardedProps.tenantId
: defaultTenantId
const context: AppContext = { userId, tenantId }
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: params.messages,
threadId: params.threadId,
context,
})
return toServerSentEventsResponse(stream)
}Warning
The client controls forwardedProps. Validate and allowlist every field before you put it in the server context.
Advanced#
How the type checks work#
The tools and middleware decide what the context must contain. The context object alone does not decide this. In this call, the tool needs UserContext and the middleware needs TenantContext, so context must have both:
import { chat, toolDefinition } from '@tanstack/ai'
import type { ChatMiddleware } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
type UserContext = { userId: string }
type TenantContext = { tenantId: string }
const currentUserTool = toolDefinition({
name: 'current_user',
description: 'Read the current user',
}).server<UserContext>((_input, ctx) => {
return { userId: ctx.context.userId }
})
const tenantMiddleware: ChatMiddleware<TenantContext> = {
name: 'tenant',
onStart(ctx) {
console.log(ctx.context.tenantId)
},
}
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: [{ role: 'user', content: 'Who am I?' }],
threadId: 'thread-1',
tools: [currentUserTool],
middleware: [tenantMiddleware],
context: {
userId: 'user_123',
tenantId: 'tenant_456',
},
})If you remove tenantId, TypeScript reports an error, because tenantMiddleware needs it.
More rules:
- Untyped tools and middleware still work. They get unknown context and do not make context required.
- The inference also works for tools and middleware that you declare outside the chat() call and pass in as arrays.
- To make the context optional, declare TContext | undefined. When all typed consumers accept undefined, you can leave out context.
- If you pass a context value, it must satisfy every typed consumer.
The same rules on the client#
useChat() checks context against the client tools. This tool declares ClientRuntimeContext & { mode: 'debug' }, so context needs currentTabId and the literal mode: 'debug':
import { toolDefinition } from '@tanstack/ai'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
type ClientRuntimeContext = {
currentTabId: string
}
const inspectClientContext = toolDefinition({
name: 'inspect_client_context',
description: 'Inspect local browser context',
}).client<ClientRuntimeContext & { mode: 'debug' }>((_input, ctx) => {
return {
tabId: ctx.context.currentTabId,
mode: ctx.context.mode,
}
})
export function DebugChat() {
const { sendMessage } = useChat({
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
tools: [inspectClientContext],
context: {
currentTabId: 'settings',
mode: 'debug',
},
})
return <button onClick={() => sendMessage('Inspect')}>Send</button>
}AG-UI Context#
AG-UI defines its own RunAgentInput.context field. It holds context entries for agents at the protocol level. chatParamsFromRequest gives you this field as params.aguiContext. It is separate from chat({ context }).
TanStack AI does not copy params.aguiContext into the runtime context. To use these values, validate them and map them yourself:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { buildRuntimeContextFrom } from './context'
import { serverTools } from './tools'
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: params.messages,
threadId: params.threadId,
tools: serverTools,
context: buildRuntimeContextFrom(params.aguiContext),
})
return toServerSentEventsResponse(stream)
}buildRuntimeContextFrom is your own function. It reads the entries you trust and returns your context object.