@tanstack/ai 0.52.3 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -13
- package/dist/esm/activities/chat/index.js +5 -3
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/generateLiveVideo/adapter.d.ts +69 -0
- package/dist/esm/activities/generateLiveVideo/adapter.js +23 -0
- package/dist/esm/activities/generateLiveVideo/adapter.js.map +1 -0
- package/dist/esm/activities/generateLiveVideo/index.d.ts +99 -0
- package/dist/esm/activities/generateLiveVideo/index.js +162 -0
- package/dist/esm/activities/generateLiveVideo/index.js.map +1 -0
- package/dist/esm/activities/generateVideo/index.js +3 -1
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateWorld/adapter.d.ts +69 -0
- package/dist/esm/activities/generateWorld/adapter.js +23 -0
- package/dist/esm/activities/generateWorld/adapter.js.map +1 -0
- package/dist/esm/activities/generateWorld/index.d.ts +99 -0
- package/dist/esm/activities/generateWorld/index.js +162 -0
- package/dist/esm/activities/generateWorld/index.js.map +1 -0
- package/dist/esm/activities/index.d.ts +8 -2
- package/dist/esm/activities/index.js +11 -7
- package/dist/esm/activities/middleware/types.d.ts +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.js +2 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/byok/define-provider.d.ts +6 -0
- package/dist/esm/byok/define-provider.js +2 -1
- package/dist/esm/byok/define-provider.js.map +1 -1
- package/dist/esm/byok/get-key.d.ts +7 -0
- package/dist/esm/byok/get-key.js +8 -1
- package/dist/esm/byok/get-key.js.map +1 -1
- package/dist/esm/byok/server.d.ts +1 -1
- package/dist/esm/byok/server.js +2 -2
- package/dist/esm/client.d.ts +4 -2
- package/dist/esm/client.js +3 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +4 -2
- package/dist/esm/index.js +3 -1
- package/dist/esm/middlewares/otel.js +3 -1
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/types.d.ts +112 -0
- package/package.json +2 -2
- package/skills/ai-core/adapter-configuration/SKILL.md +103 -54
- package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +39 -21
- package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +5 -0
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +14 -6
- package/skills/ai-core/adapter-configuration/references/grok-adapter.md +33 -25
- package/skills/ai-core/adapter-configuration/references/groq-adapter.md +7 -2
- package/skills/ai-core/adapter-configuration/references/ollama-adapter.md +25 -12
- package/skills/ai-core/adapter-configuration/references/openai-adapter.md +19 -9
- package/skills/ai-core/adapter-configuration/references/openrouter-adapter.md +34 -21
- package/skills/ai-core/ag-ui-protocol/SKILL.md +16 -10
- package/skills/ai-core/chat-experience/SKILL.md +228 -108
- package/skills/ai-core/client-persistence/SKILL.md +21 -9
- package/skills/ai-core/custom-backend-integration/SKILL.md +86 -52
- package/skills/ai-core/debug-logging/SKILL.md +100 -18
- package/skills/ai-core/locks/SKILL.md +35 -7
- package/skills/ai-core/media-generation/SKILL.md +114 -49
- package/skills/ai-core/middleware/SKILL.md +174 -69
- package/skills/ai-core/structured-outputs/SKILL.md +99 -49
- package/skills/ai-core/tool-calling/SKILL.md +245 -158
- package/src/activities/chat/index.ts +6 -7
- package/src/activities/generateLiveVideo/adapter.ts +99 -0
- package/src/activities/generateLiveVideo/index.ts +339 -0
- package/src/activities/generateVideo/index.ts +3 -4
- package/src/activities/generateWorld/adapter.ts +96 -0
- package/src/activities/generateWorld/index.ts +339 -0
- package/src/activities/index.ts +44 -0
- package/src/activities/middleware/types.ts +2 -0
- package/src/activities/summarize/chat-stream-summarize.ts +2 -0
- package/src/byok/define-provider.ts +7 -0
- package/src/byok/get-key.ts +18 -0
- package/src/byok/server.ts +1 -1
- package/src/client.ts +8 -0
- package/src/index.ts +8 -0
- package/src/middlewares/otel.ts +2 -0
- package/src/types.ts +128 -0
|
@@ -26,8 +26,9 @@ This skill builds on ai-core. Read it first for critical rules.
|
|
|
26
26
|
## Setup
|
|
27
27
|
|
|
28
28
|
Complete end-to-end example: shared definition, server tool, client tool, server route, React client.
|
|
29
|
+
The four files below share one scope, so later files use the earlier exports directly.
|
|
29
30
|
|
|
30
|
-
```typescript
|
|
31
|
+
```typescript group=product-catalog
|
|
31
32
|
// tools/definitions.ts
|
|
32
33
|
import { toolDefinition } from '@tanstack/ai'
|
|
33
34
|
import { z } from 'zod'
|
|
@@ -54,24 +55,23 @@ export const updateCartUIDef = toolDefinition({
|
|
|
54
55
|
})
|
|
55
56
|
```
|
|
56
57
|
|
|
57
|
-
```typescript
|
|
58
|
-
// tools/server.ts
|
|
59
|
-
import {
|
|
58
|
+
```typescript group=product-catalog
|
|
59
|
+
// tools/server.ts (uses getProductsDef from tools/definitions.ts)
|
|
60
|
+
import { db } from './db'
|
|
60
61
|
|
|
61
62
|
export const getProducts = getProductsDef.server(async ({ query, limit }) => {
|
|
62
|
-
const results
|
|
63
|
+
const results: Array<{ id: string; name: string; price: number }> =
|
|
64
|
+
await db.products.search(query, { limit: limit ?? 10 })
|
|
63
65
|
return {
|
|
64
66
|
products: results.map((p) => ({ id: p.id, name: p.name, price: p.price })),
|
|
65
67
|
}
|
|
66
68
|
})
|
|
67
69
|
```
|
|
68
70
|
|
|
69
|
-
```typescript
|
|
70
|
-
// api/chat/route.ts
|
|
71
|
+
```typescript group=product-catalog
|
|
72
|
+
// api/chat/route.ts (uses getProducts and updateCartUIDef from tools/)
|
|
71
73
|
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
72
74
|
import { openaiText } from '@tanstack/ai-openai'
|
|
73
|
-
import { getProducts } from '@/tools/server'
|
|
74
|
-
import { updateCartUIDef } from '@/tools/definitions'
|
|
75
75
|
|
|
76
76
|
export async function POST(request: Request) {
|
|
77
77
|
const { messages } = await request.json()
|
|
@@ -84,32 +84,31 @@ export async function POST(request: Request) {
|
|
|
84
84
|
}
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
```
|
|
88
|
-
// app/chat.tsx
|
|
87
|
+
```tsx group=product-catalog
|
|
88
|
+
// app/chat.tsx (uses updateCartUIDef from tools/definitions.ts)
|
|
89
89
|
import {
|
|
90
90
|
useChat,
|
|
91
91
|
fetchServerSentEvents,
|
|
92
|
-
clientTools,
|
|
93
92
|
createChatClientOptions,
|
|
94
93
|
type InferChatMessages,
|
|
95
|
-
} from
|
|
96
|
-
import {
|
|
97
|
-
import { useState } from
|
|
94
|
+
} from '@tanstack/ai-react'
|
|
95
|
+
import { clientTools } from '@tanstack/ai-client'
|
|
96
|
+
import { useState } from 'react'
|
|
98
97
|
|
|
99
98
|
function ChatPage() {
|
|
100
|
-
const [cartCount, setCartCount] = useState(0)
|
|
99
|
+
const [cartCount, setCartCount] = useState(0)
|
|
101
100
|
|
|
102
101
|
const updateCartUI = updateCartUIDef.client((input) => {
|
|
103
|
-
setCartCount(input.itemCount)
|
|
104
|
-
return { displayed: true }
|
|
105
|
-
})
|
|
102
|
+
setCartCount(input.itemCount)
|
|
103
|
+
return { displayed: true }
|
|
104
|
+
})
|
|
106
105
|
|
|
107
|
-
const tools = clientTools(updateCartUI)
|
|
106
|
+
const tools = clientTools(updateCartUI)
|
|
108
107
|
const chatOptions = createChatClientOptions({
|
|
109
|
-
connection: fetchServerSentEvents(
|
|
108
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
110
109
|
tools,
|
|
111
|
-
})
|
|
112
|
-
const { messages, sendMessage } = useChat(chatOptions)
|
|
110
|
+
})
|
|
111
|
+
const { messages, sendMessage } = useChat(chatOptions)
|
|
113
112
|
// InferChatMessages ties part types to the configured tools when needed:
|
|
114
113
|
// type Messages = InferChatMessages<typeof chatOptions>
|
|
115
114
|
|
|
@@ -119,16 +118,20 @@ function ChatPage() {
|
|
|
119
118
|
{messages.map((msg) => (
|
|
120
119
|
<div key={msg.id}>
|
|
121
120
|
{msg.parts.map((part) => {
|
|
122
|
-
if (part.type ===
|
|
123
|
-
if (part.type ===
|
|
124
|
-
return
|
|
121
|
+
if (part.type === 'text') return <p>{part.content}</p>
|
|
122
|
+
if (part.type === 'tool-call') {
|
|
123
|
+
return (
|
|
124
|
+
<div key={part.id}>
|
|
125
|
+
Tool: {part.name} ({part.state})
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
125
128
|
}
|
|
126
|
-
return null
|
|
129
|
+
return null
|
|
127
130
|
})}
|
|
128
131
|
</div>
|
|
129
132
|
))}
|
|
130
133
|
</div>
|
|
131
|
-
)
|
|
134
|
+
)
|
|
132
135
|
}
|
|
133
136
|
```
|
|
134
137
|
|
|
@@ -192,8 +195,10 @@ Define with `toolDefinition()`, implement with `.server()`, pass to `chat({ tool
|
|
|
192
195
|
The server executes it automatically. The client never runs code for this tool.
|
|
193
196
|
|
|
194
197
|
```typescript
|
|
195
|
-
import { toolDefinition } from '@tanstack/ai'
|
|
198
|
+
import { chat, toolDefinition, toServerSentEventsResponse } from '@tanstack/ai'
|
|
199
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
196
200
|
import { z } from 'zod'
|
|
201
|
+
import { db } from './db'
|
|
197
202
|
|
|
198
203
|
const getUserDataDef = toolDefinition({
|
|
199
204
|
name: 'get_user_data',
|
|
@@ -210,11 +215,15 @@ const getUserData = getUserDataDef.server(async ({ userId }) => {
|
|
|
210
215
|
})
|
|
211
216
|
|
|
212
217
|
// In your route handler:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
export async function POST(request: Request) {
|
|
219
|
+
const { messages } = await request.json()
|
|
220
|
+
const stream = chat({
|
|
221
|
+
adapter: openaiText('gpt-5.5'),
|
|
222
|
+
messages,
|
|
223
|
+
tools: [getUserData],
|
|
224
|
+
})
|
|
225
|
+
return toServerSentEventsResponse(stream)
|
|
226
|
+
}
|
|
218
227
|
```
|
|
219
228
|
|
|
220
229
|
### Pattern 2: Client-Only Tool
|
|
@@ -222,7 +231,8 @@ const stream = chat({
|
|
|
222
231
|
Pass the bare definition (no `.server()`) to `chat({ tools })` so the LLM knows
|
|
223
232
|
about it. Pass the `.client()` implementation to `useChat` via `clientTools()`.
|
|
224
233
|
|
|
225
|
-
```typescript
|
|
234
|
+
```typescript group=notification-tool
|
|
235
|
+
// tools/definitions.ts
|
|
226
236
|
import { toolDefinition } from '@tanstack/ai'
|
|
227
237
|
import { z } from 'zod'
|
|
228
238
|
|
|
@@ -239,41 +249,49 @@ export const showNotificationDef = toolDefinition({
|
|
|
239
249
|
|
|
240
250
|
Server -- pass definition only (no execute function):
|
|
241
251
|
|
|
242
|
-
```typescript
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
252
|
+
```typescript group=notification-tool
|
|
253
|
+
// api/chat/route.ts (uses showNotificationDef from tools/definitions.ts)
|
|
254
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
255
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
256
|
+
|
|
257
|
+
export async function POST(request: Request) {
|
|
258
|
+
const { messages } = await request.json()
|
|
259
|
+
const stream = chat({
|
|
260
|
+
adapter: openaiText('gpt-5.5'),
|
|
261
|
+
messages,
|
|
262
|
+
tools: [showNotificationDef],
|
|
263
|
+
})
|
|
264
|
+
return toServerSentEventsResponse(stream)
|
|
265
|
+
}
|
|
248
266
|
```
|
|
249
267
|
|
|
250
268
|
Client -- pass `.client()` implementation:
|
|
251
269
|
|
|
252
|
-
```
|
|
270
|
+
```tsx group=notification-tool
|
|
271
|
+
// app/chat.tsx (uses showNotificationDef from tools/definitions.ts)
|
|
253
272
|
import {
|
|
254
273
|
useChat,
|
|
255
274
|
fetchServerSentEvents,
|
|
256
|
-
clientTools,
|
|
257
275
|
createChatClientOptions,
|
|
258
|
-
} from
|
|
259
|
-
import {
|
|
260
|
-
import { useState } from
|
|
276
|
+
} from '@tanstack/ai-react'
|
|
277
|
+
import { clientTools } from '@tanstack/ai-client'
|
|
278
|
+
import { useState } from 'react'
|
|
261
279
|
|
|
262
280
|
function ChatPage() {
|
|
263
|
-
const [toast, setToast] = useState<string | null>(null)
|
|
281
|
+
const [toast, setToast] = useState<string | null>(null)
|
|
264
282
|
|
|
265
283
|
const showNotification = showNotificationDef.client((input) => {
|
|
266
|
-
setToast(input.message)
|
|
267
|
-
setTimeout(() => setToast(null), 3000)
|
|
268
|
-
return { shown: true }
|
|
269
|
-
})
|
|
284
|
+
setToast(input.message)
|
|
285
|
+
setTimeout(() => setToast(null), 3000)
|
|
286
|
+
return { shown: true }
|
|
287
|
+
})
|
|
270
288
|
|
|
271
289
|
const { messages, sendMessage } = useChat(
|
|
272
290
|
createChatClientOptions({
|
|
273
|
-
connection: fetchServerSentEvents(
|
|
291
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
274
292
|
tools: clientTools(showNotification),
|
|
275
|
-
})
|
|
276
|
-
)
|
|
293
|
+
}),
|
|
294
|
+
)
|
|
277
295
|
|
|
278
296
|
return (
|
|
279
297
|
<div>
|
|
@@ -281,12 +299,12 @@ function ChatPage() {
|
|
|
281
299
|
{messages.map((msg) => (
|
|
282
300
|
<div key={msg.id}>
|
|
283
301
|
{msg.parts.map((part) =>
|
|
284
|
-
part.type ===
|
|
302
|
+
part.type === 'text' ? <p>{part.content}</p> : null,
|
|
285
303
|
)}
|
|
286
304
|
</div>
|
|
287
305
|
))}
|
|
288
306
|
</div>
|
|
289
|
-
)
|
|
307
|
+
)
|
|
290
308
|
}
|
|
291
309
|
```
|
|
292
310
|
|
|
@@ -298,9 +316,11 @@ Set `needsApproval: true` in the definition. Execution pauses with
|
|
|
298
316
|
`addToolApprovalResponse` and `pendingInterrupts` remain as deprecated
|
|
299
317
|
compatibility shims during migration.
|
|
300
318
|
|
|
301
|
-
```typescript
|
|
319
|
+
```typescript group=email-approval
|
|
320
|
+
// tools/email.ts
|
|
302
321
|
import { toolDefinition } from '@tanstack/ai'
|
|
303
322
|
import { z } from 'zod'
|
|
323
|
+
import { emailService } from './email-service'
|
|
304
324
|
|
|
305
325
|
export const sendEmailDef = toolDefinition({
|
|
306
326
|
name: 'send_email',
|
|
@@ -323,18 +343,20 @@ export const sendEmail = sendEmailDef.server(async ({ to, subject, body }) => {
|
|
|
323
343
|
Server route must forward `resume` / `parentRunId` (via `chatParamsFromRequest`
|
|
324
344
|
or equivalent). Client -- render bound interrupts:
|
|
325
345
|
|
|
326
|
-
```
|
|
327
|
-
|
|
346
|
+
```tsx group=email-approval
|
|
347
|
+
// app/chat.tsx (registers sendEmailDef so the approval interrupt is typed)
|
|
348
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
328
349
|
|
|
329
350
|
function ChatPage() {
|
|
330
351
|
const { messages, interrupts, sendMessage } = useChat({
|
|
331
|
-
connection: fetchServerSentEvents(
|
|
332
|
-
|
|
352
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
353
|
+
tools: [sendEmailDef],
|
|
354
|
+
})
|
|
333
355
|
|
|
334
356
|
return (
|
|
335
357
|
<div>
|
|
336
358
|
{interrupts.map((interrupt) => {
|
|
337
|
-
if (interrupt.kind !==
|
|
359
|
+
if (interrupt.kind !== 'tool-approval') return null
|
|
338
360
|
return (
|
|
339
361
|
<div key={interrupt.id}>
|
|
340
362
|
<p>Approve "{interrupt.toolName}"?</p>
|
|
@@ -347,33 +369,54 @@ function ChatPage() {
|
|
|
347
369
|
</button>
|
|
348
370
|
<button onClick={() => interrupt.cancel()}>Cancel</button>
|
|
349
371
|
</div>
|
|
350
|
-
)
|
|
372
|
+
)
|
|
351
373
|
})}
|
|
352
374
|
{messages.map((msg) => (
|
|
353
375
|
<div key={msg.id}>
|
|
354
376
|
{msg.parts.map((part) =>
|
|
355
|
-
part.type ===
|
|
377
|
+
part.type === 'text' ? (
|
|
378
|
+
<p key={part.content}>{part.content}</p>
|
|
379
|
+
) : null,
|
|
356
380
|
)}
|
|
357
381
|
</div>
|
|
358
382
|
))}
|
|
359
383
|
</div>
|
|
360
|
-
)
|
|
384
|
+
)
|
|
361
385
|
}
|
|
362
386
|
```
|
|
363
387
|
|
|
364
388
|
Batch all pending approvals with `resolveInterrupts` (void — submission is
|
|
365
389
|
async; watch `resuming` / `interruptErrors`):
|
|
366
390
|
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
resolveInterrupts(
|
|
391
|
+
```tsx group=email-approval
|
|
392
|
+
function ApproveAllButton() {
|
|
393
|
+
const { resolveInterrupts, resuming } = useChat({
|
|
394
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
395
|
+
tools: [sendEmailDef],
|
|
396
|
+
})
|
|
370
397
|
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
398
|
+
// Payloadless tool-approvals only
|
|
399
|
+
const approveAll = () => resolveInterrupts(true)
|
|
400
|
+
|
|
401
|
+
// Or per-item:
|
|
402
|
+
const approveEach = () =>
|
|
403
|
+
resolveInterrupts((interrupt) => {
|
|
404
|
+
if (interrupt.kind === 'tool-approval') {
|
|
405
|
+
interrupt.resolveInterrupt(true)
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<>
|
|
411
|
+
<button disabled={resuming} onClick={approveAll}>
|
|
412
|
+
Approve all
|
|
413
|
+
</button>
|
|
414
|
+
<button disabled={resuming} onClick={approveEach}>
|
|
415
|
+
Approve each
|
|
416
|
+
</button>
|
|
417
|
+
</>
|
|
418
|
+
)
|
|
419
|
+
}
|
|
377
420
|
```
|
|
378
421
|
|
|
379
422
|
Migration: `pendingInterrupts` aliases `interrupts`; `addToolApprovalResponse`
|
|
@@ -385,15 +428,17 @@ above for new code. See `docs/interrupts/`.
|
|
|
385
428
|
Set `lazy: true` on rarely-needed tools. The LLM sees their names via a synthetic
|
|
386
429
|
`__lazy__tool__discovery__` tool and discovers schemas on demand. Saves tokens.
|
|
387
430
|
|
|
388
|
-
```typescript
|
|
431
|
+
```typescript group=lazy-tools
|
|
389
432
|
import {
|
|
390
433
|
toolDefinition,
|
|
391
434
|
chat,
|
|
392
435
|
toServerSentEventsResponse,
|
|
393
436
|
maxIterations,
|
|
437
|
+
type ModelMessage,
|
|
394
438
|
} from '@tanstack/ai'
|
|
395
439
|
import { openaiText } from '@tanstack/ai-openai'
|
|
396
440
|
import { z } from 'zod'
|
|
441
|
+
import { db } from './db'
|
|
397
442
|
|
|
398
443
|
const getProductsDef = toolDefinition({
|
|
399
444
|
name: 'getProducts',
|
|
@@ -440,14 +485,17 @@ When all lazy tools are discovered, the discovery tool is removed automatically.
|
|
|
440
485
|
By default the discovery-tool catalog lists only bare names (`'none'`). Pass
|
|
441
486
|
`lazyToolsConfig` to `chat()` to include more context:
|
|
442
487
|
|
|
443
|
-
```typescript
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
488
|
+
```typescript group=lazy-tools
|
|
489
|
+
// Same tools as the route above, with a richer discovery catalog:
|
|
490
|
+
export function chatWithCatalog(messages: Array<ModelMessage>) {
|
|
491
|
+
return chat({
|
|
492
|
+
adapter: openaiText('gpt-5.5'),
|
|
493
|
+
messages,
|
|
494
|
+
tools: [getProducts, compareProducts],
|
|
495
|
+
agentLoopStrategy: maxIterations(20),
|
|
496
|
+
lazyToolsConfig: { includeDescription: 'first-sentence' },
|
|
497
|
+
})
|
|
498
|
+
}
|
|
451
499
|
```
|
|
452
500
|
|
|
453
501
|
`includeDescription` values:
|
|
@@ -475,48 +523,41 @@ See the `@tanstack/ai-mcp` skill for the full MCP Apps API
|
|
|
475
523
|
### Basic usage — auto-discovery
|
|
476
524
|
|
|
477
525
|
```typescript
|
|
478
|
-
//
|
|
479
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
526
|
+
// api/chat/route.ts
|
|
480
527
|
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
481
528
|
import { openaiText } from '@tanstack/ai-openai'
|
|
482
529
|
import { createMCPClient } from '@tanstack/ai-mcp'
|
|
483
530
|
|
|
484
|
-
export
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
onFinish: () => mcp.close(),
|
|
510
|
-
onAbort: () => mcp.close(),
|
|
511
|
-
onError: () => mcp.close(),
|
|
512
|
-
},
|
|
513
|
-
],
|
|
514
|
-
})
|
|
515
|
-
return toServerSentEventsResponse(stream)
|
|
531
|
+
export async function POST(request: Request) {
|
|
532
|
+
const { messages } = await request.json()
|
|
533
|
+
|
|
534
|
+
// 1. Connect to the MCP server.
|
|
535
|
+
const mcp = await createMCPClient({
|
|
536
|
+
transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
// 2. Discover all tools from the server (returns ServerTool[]).
|
|
540
|
+
const mcpTools = await mcp.tools()
|
|
541
|
+
|
|
542
|
+
// 3. Spread them into chat() — they work exactly like hand-written tools.
|
|
543
|
+
// Caller owns the lifecycle — chat() never closes the client. Tools run
|
|
544
|
+
// while the response streams, so close in a middleware terminal hook
|
|
545
|
+
// (a try/finally around the return would close before tools execute).
|
|
546
|
+
const stream = chat({
|
|
547
|
+
adapter: openaiText('gpt-5.5'),
|
|
548
|
+
messages,
|
|
549
|
+
tools: [...mcpTools],
|
|
550
|
+
middleware: [
|
|
551
|
+
{
|
|
552
|
+
name: 'mcp-close',
|
|
553
|
+
onFinish: () => mcp.close(),
|
|
554
|
+
onAbort: () => mcp.close(),
|
|
555
|
+
onError: () => mcp.close(),
|
|
516
556
|
},
|
|
517
|
-
|
|
518
|
-
}
|
|
519
|
-
|
|
557
|
+
],
|
|
558
|
+
})
|
|
559
|
+
return toServerSentEventsResponse(stream)
|
|
560
|
+
}
|
|
520
561
|
```
|
|
521
562
|
|
|
522
563
|
### Typed path — pass toolDefinition instances
|
|
@@ -526,7 +567,8 @@ The MCP client supplies a `callTool` proxy as the execute function, while
|
|
|
526
567
|
input/output validation and types come from the definitions' Zod schemas.
|
|
527
568
|
|
|
528
569
|
```typescript
|
|
529
|
-
import { toolDefinition } from '@tanstack/ai'
|
|
570
|
+
import { chat, toolDefinition } from '@tanstack/ai'
|
|
571
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
530
572
|
import { createMCPClient } from '@tanstack/ai-mcp'
|
|
531
573
|
import { z } from 'zod'
|
|
532
574
|
|
|
@@ -545,12 +587,15 @@ const mcp = await createMCPClient({
|
|
|
545
587
|
// Throws MCPToolNotFoundError if the server does not expose a tool with that name.
|
|
546
588
|
const tools = await mcp.tools([getWeather])
|
|
547
589
|
|
|
590
|
+
const messages = [{ role: 'user' as const, content: 'Weather in Paris?' }]
|
|
548
591
|
const stream = chat({ adapter: openaiText('gpt-5.5'), messages, tools })
|
|
549
592
|
```
|
|
550
593
|
|
|
551
594
|
### Multiple servers with `createMCPClients`
|
|
552
595
|
|
|
553
596
|
```typescript
|
|
597
|
+
import { chat } from '@tanstack/ai'
|
|
598
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
554
599
|
import { createMCPClients } from '@tanstack/ai-mcp'
|
|
555
600
|
|
|
556
601
|
// Each key becomes the default prefix for that server's tools.
|
|
@@ -562,6 +607,7 @@ await using pool = await createMCPClients({
|
|
|
562
607
|
// Tools auto-prefixed: 'github_search_repos', 'linear_create_issue', etc.
|
|
563
608
|
const tools = await pool.tools()
|
|
564
609
|
|
|
610
|
+
const messages = [{ role: 'user' as const, content: 'Open an issue for #42' }]
|
|
565
611
|
const stream = chat({ adapter: openaiText('gpt-5.5'), messages, tools })
|
|
566
612
|
```
|
|
567
613
|
|
|
@@ -578,9 +624,18 @@ cancelled automatically.
|
|
|
578
624
|
You can also forward it from your own server tools:
|
|
579
625
|
|
|
580
626
|
```typescript
|
|
581
|
-
|
|
627
|
+
import { toolDefinition } from '@tanstack/ai'
|
|
628
|
+
import { z } from 'zod'
|
|
629
|
+
|
|
630
|
+
const fetchReportDef = toolDefinition({
|
|
631
|
+
name: 'fetch_report',
|
|
632
|
+
description: 'Fetch a report from the slow reporting API',
|
|
633
|
+
inputSchema: z.object({ reportId: z.string() }),
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
const fetchReport = fetchReportDef.server(async ({ reportId }, ctx) => {
|
|
582
637
|
// Forward to fetch, a DB query, or an MCP callTool call.
|
|
583
|
-
const response = await fetch(
|
|
638
|
+
const response = await fetch(`https://slow.api/reports/${reportId}`, {
|
|
584
639
|
signal: ctx?.abortSignal,
|
|
585
640
|
})
|
|
586
641
|
return response.json()
|
|
@@ -639,39 +694,33 @@ Instead of manually calling `client.tools()` and managing `close()`, pass an
|
|
|
639
694
|
**Example:**
|
|
640
695
|
|
|
641
696
|
```typescript
|
|
642
|
-
|
|
697
|
+
// api/chat/route.ts
|
|
643
698
|
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
644
699
|
import { openaiText } from '@tanstack/ai-openai'
|
|
645
700
|
import { createMCPClient } from '@tanstack/ai-mcp'
|
|
646
701
|
|
|
647
|
-
export
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
onDiscoveryError: (err, source) => {
|
|
664
|
-
console.warn('MCP discovery failed, skipping source:', err)
|
|
665
|
-
// returning (not throwing) skips this source and continues
|
|
666
|
-
},
|
|
667
|
-
},
|
|
668
|
-
})
|
|
669
|
-
|
|
670
|
-
return toServerSentEventsResponse(stream)
|
|
702
|
+
export async function POST(request: Request) {
|
|
703
|
+
const { messages } = await request.json()
|
|
704
|
+
|
|
705
|
+
const mcpClient = await createMCPClient({
|
|
706
|
+
transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
const stream = chat({
|
|
710
|
+
adapter: openaiText('gpt-5.5'),
|
|
711
|
+
messages,
|
|
712
|
+
mcp: {
|
|
713
|
+
clients: [mcpClient],
|
|
714
|
+
connection: 'keep-alive',
|
|
715
|
+
onDiscoveryError: (err) => {
|
|
716
|
+
console.warn('MCP discovery failed, skipping source:', err)
|
|
717
|
+
// returning (not throwing) skips this source and continues
|
|
671
718
|
},
|
|
672
719
|
},
|
|
673
|
-
}
|
|
674
|
-
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
return toServerSentEventsResponse(stream)
|
|
723
|
+
}
|
|
675
724
|
```
|
|
676
725
|
|
|
677
726
|
## Provider Skills
|
|
@@ -763,23 +812,61 @@ Server tools need `chat({ tools })`. Client tools need their definition in
|
|
|
763
812
|
|
|
764
813
|
Wrong -- tool only on server, client cannot execute:
|
|
765
814
|
|
|
766
|
-
```
|
|
815
|
+
```tsx group=tool-wiring
|
|
816
|
+
import { chat, toolDefinition } from '@tanstack/ai'
|
|
817
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
818
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
819
|
+
import { clientTools } from '@tanstack/ai-client'
|
|
820
|
+
import { z } from 'zod'
|
|
821
|
+
|
|
822
|
+
const myToolDef = toolDefinition({
|
|
823
|
+
name: 'my_tool',
|
|
824
|
+
description: 'Example client-executed tool',
|
|
825
|
+
inputSchema: z.object({ id: z.string() }),
|
|
826
|
+
outputSchema: z.object({ success: z.boolean() }),
|
|
827
|
+
})
|
|
828
|
+
const adapter = openaiText('gpt-5.5')
|
|
829
|
+
const messages = [{ role: 'user' as const, content: 'Run my tool' }]
|
|
830
|
+
|
|
831
|
+
// server
|
|
767
832
|
chat({ adapter, messages, tools: [myToolDef] })
|
|
768
|
-
|
|
833
|
+
// client
|
|
834
|
+
function ChatServerOnly() {
|
|
835
|
+
useChat({ connection: fetchServerSentEvents('/api/chat') }) // no tools
|
|
836
|
+
return null
|
|
837
|
+
}
|
|
769
838
|
```
|
|
770
839
|
|
|
771
840
|
Wrong -- tool only on client, LLM does not know about it:
|
|
772
841
|
|
|
773
|
-
```
|
|
774
|
-
|
|
775
|
-
|
|
842
|
+
```tsx group=tool-wiring
|
|
843
|
+
// server
|
|
844
|
+
chat({ adapter, messages }) // no tools
|
|
845
|
+
// client
|
|
846
|
+
function ChatClientOnly() {
|
|
847
|
+
useChat({
|
|
848
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
849
|
+
tools: clientTools(myToolDef.client(() => ({ success: true }))),
|
|
850
|
+
})
|
|
851
|
+
return null
|
|
852
|
+
}
|
|
776
853
|
```
|
|
777
854
|
|
|
778
855
|
Correct:
|
|
779
856
|
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
|
|
857
|
+
```tsx group=tool-wiring
|
|
858
|
+
// server
|
|
859
|
+
chat({ adapter, messages, tools: [myToolDef] })
|
|
860
|
+
// client
|
|
861
|
+
function ChatWired() {
|
|
862
|
+
useChat({
|
|
863
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
864
|
+
tools: clientTools(
|
|
865
|
+
myToolDef.client((input) => ({ success: input.id !== '' })),
|
|
866
|
+
),
|
|
867
|
+
})
|
|
868
|
+
return null
|
|
869
|
+
}
|
|
783
870
|
```
|
|
784
871
|
|
|
785
872
|
Source: docs/tools/tools.md
|