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.
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) ?? []
})// 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.
The tools and middleware are the contract. The context value is what you supply at runtime.
Declare the type in one of these places:
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 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.
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.
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:
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 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.