Install @tanstack/ai-octane, then call useChat the same way you would in React. The hook modules are .tsrx and compile in your Octane plugin.
npm i @tanstack/ai-octane octanepnpm add @tanstack/ai-octane octaneyarn add @tanstack/ai-octane octanebun add @tanstack/ai-octane octaneoctane is a required peer. This package publishes uncompiled source, like Svelte packages that ship .svelte.
Register executable client tools after the Octane component mounts. Octane removes them on cleanup and replaces them when tools or options change.
For a complete setup and behavior guide, see WebMCP Tools.
import {
useWebMCPTools,
type UseWebMCPToolsOptions,
} from '@tanstack/ai-octane'
import { searchProducts } from './tools'
const tools = [searchProducts]
const options: UseWebMCPToolsOptions<typeof tools> = {
onError(error) {
console.error(error)
},
}
function ProductsPage() {
useWebMCPTools(tools, options)
return null
}UseWebMCPToolsOptions<TTools, TContext> contains toolOptions, context, and onError. The hook owns the registration signal.
The context field is required when a tool declares a required runtime context. Keep tools and options stable when their values do not change.
Manages chat state in an Octane component.
import { useState } from 'octane'
import { useChat, fetchServerSentEvents } from '@tanstack/ai-octane'
import {
createChatClientOptions,
type InferChatMessages,
} from '@tanstack/ai-client'
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
const updateUIDef = toolDefinition({
name: 'updateUI',
description: 'Show a notification in the UI',
inputSchema: z.object({ message: z.string() }),
})
export function ChatComponent() {
const [notification, setNotification] = useState<string | null>(null)
const updateUI = updateUIDef.client((input) => {
setNotification(input.message)
return { success: true }
})
const tools = [updateUI]
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents('/api/chat'),
tools,
})
type ChatMessages = InferChatMessages<typeof chatOptions>
const { messages, sendMessage, isLoading, error, addToolApprovalResponse } =
useChat(chatOptions)
return (
<div>
{notification}
{isLoading ? 'Loading' : null}
{error ? error.message : null}
<button onClick={() => void sendMessage('hi')} type="button">
Send
</button>
<button
onClick={() =>
void addToolApprovalResponse({ id: 'approval-1', approved: true })
}
type="button"
>
Approve
</button>
{messages.length}
</div>
)
}The matching server route still runs chat({ adapter, messages }) and returns SSE. See Quick Start.
Extends ChatClientOptions from @tanstack/ai-client. Pass connection or fetcher, not both.
Client tools run automatically. There is no onToolCall callback.
Changing connection or fetcher updates the live ChatClient. Changing threadId creates a new client.
import type { UIMessage } from '@tanstack/ai-octane'
import type { ModelMessage } from '@tanstack/ai/client'
import type {
MultimodalContent,
ChatClientState,
ConnectionStatus,
QueuedMessage,
SendMessageOptions,
} from '@tanstack/ai-client'
interface UseChatReturn {
messages: Array<UIMessage>
sendMessage: (
content: string | MultimodalContent,
options?: SendMessageOptions,
) => Promise<void>
append: (message: ModelMessage | UIMessage) => Promise<void>
addToolResult: (result: {
toolCallId: string
tool: string
output: unknown
state?: 'output-available' | 'output-error'
errorText?: string
}) => Promise<void>
addToolApprovalResponse: (response: {
id: string
approved: boolean
}) => Promise<void>
reload: () => Promise<void>
stop: () => void
isLoading: boolean
error: Error | undefined
status: ChatClientState
isSubscribed: boolean
connectionStatus: ConnectionStatus
sessionGenerating: boolean
setMessages: (messages: Array<UIMessage>) => void
clear: () => void
queue: Array<QueuedMessage>
cancelQueued: (id: string) => void
runId: string | null
}queue holds sends that wait while a run is busy. runId is the in-flight turn, or null. Interrupt helpers (interrupts, resolveInterrupts, cancelInterrupts, retryInterrupts) are on the same object when you pass interrupts.
Re-exported from @tanstack/ai-client:
import {
fetchServerSentEvents,
fetchHttpStream,
stream,
type ConnectionAdapter,
} from '@tanstack/ai-octane'import { useState } from 'octane'
import { useChat, fetchServerSentEvents } from '@tanstack/ai-octane'
export function Chat() {
const [input, setInput] = useState('')
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents('/api/chat'),
})
return (
<div>
{messages.map((message) => (
<div key={message.id}>
<strong>{message.role}:</strong>
{message.parts
.filter((part) => part.type === 'text')
.map((part) => part.content)
.join('')}
</div>
))}
<input
value={input}
disabled={isLoading}
onInput={(event) => setInput(event.currentTarget.value)}
/>
<button
disabled={isLoading}
onClick={() => {
void sendMessage(input)
setInput('')
}}
type="button"
>
Send
</button>
</div>
)
}import { useChat, fetchServerSentEvents } from '@tanstack/ai-octane'
export function ChatWithApproval() {
const { messages, sendMessage, addToolApprovalResponse } = useChat({
connection: fetchServerSentEvents('/api/chat'),
})
return (
<div>
<button onClick={() => void sendMessage('run the tool')} type="button">
Send
</button>
{messages.map((message) =>
message.parts.map((part) => {
if (
part.type !== 'tool-call' ||
part.state !== 'approval-requested' ||
!part.approval
) {
return null
}
const approvalId = part.approval.id
return (
<div key={part.id}>
<p>Approve: {part.name}</p>
<button
onClick={() =>
void addToolApprovalResponse({
id: approvalId,
approved: true,
})
}
type="button"
>
Approve
</button>
<button
onClick={() =>
void addToolApprovalResponse({
id: approvalId,
approved: false,
})
}
type="button"
>
Deny
</button>
</div>
)
}),
)}
</div>
)
}The package also exports useRealtimeChat, useMcpAppBridge, useGeneration, useGenerateImage, useGenerateAudio, useGenerateSpeech, useGenerateVideo, useTranscription, useSummarize, and useAudioRecorder.
The ./mcp-apps React AppRenderer subpath is not in this package. useMcpAppBridge is.
Re-exported from @tanstack/ai-client: