@tanstack/ai 0.53.0 → 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.
Files changed (60) hide show
  1. package/README.md +14 -13
  2. package/dist/esm/activities/chat/index.js +5 -3
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/generateLiveVideo/adapter.d.ts +69 -0
  5. package/dist/esm/activities/generateLiveVideo/adapter.js +23 -0
  6. package/dist/esm/activities/generateLiveVideo/adapter.js.map +1 -0
  7. package/dist/esm/activities/generateLiveVideo/index.d.ts +99 -0
  8. package/dist/esm/activities/generateLiveVideo/index.js +162 -0
  9. package/dist/esm/activities/generateLiveVideo/index.js.map +1 -0
  10. package/dist/esm/activities/generateVideo/index.js +3 -1
  11. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  12. package/dist/esm/activities/generateWorld/adapter.d.ts +69 -0
  13. package/dist/esm/activities/generateWorld/adapter.js +23 -0
  14. package/dist/esm/activities/generateWorld/adapter.js.map +1 -0
  15. package/dist/esm/activities/generateWorld/index.d.ts +99 -0
  16. package/dist/esm/activities/generateWorld/index.js +162 -0
  17. package/dist/esm/activities/generateWorld/index.js.map +1 -0
  18. package/dist/esm/activities/index.d.ts +8 -2
  19. package/dist/esm/activities/index.js +11 -7
  20. package/dist/esm/activities/middleware/types.d.ts +1 -1
  21. package/dist/esm/client.d.ts +4 -2
  22. package/dist/esm/client.js +3 -1
  23. package/dist/esm/client.js.map +1 -1
  24. package/dist/esm/index.d.ts +4 -2
  25. package/dist/esm/index.js +3 -1
  26. package/dist/esm/middlewares/otel.js +3 -1
  27. package/dist/esm/middlewares/otel.js.map +1 -1
  28. package/dist/esm/types.d.ts +112 -0
  29. package/package.json +2 -2
  30. package/skills/ai-core/adapter-configuration/SKILL.md +90 -42
  31. package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +39 -21
  32. package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +5 -0
  33. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +14 -6
  34. package/skills/ai-core/adapter-configuration/references/grok-adapter.md +33 -25
  35. package/skills/ai-core/adapter-configuration/references/groq-adapter.md +7 -2
  36. package/skills/ai-core/adapter-configuration/references/ollama-adapter.md +25 -12
  37. package/skills/ai-core/adapter-configuration/references/openai-adapter.md +19 -9
  38. package/skills/ai-core/adapter-configuration/references/openrouter-adapter.md +34 -21
  39. package/skills/ai-core/ag-ui-protocol/SKILL.md +16 -10
  40. package/skills/ai-core/chat-experience/SKILL.md +228 -108
  41. package/skills/ai-core/client-persistence/SKILL.md +21 -9
  42. package/skills/ai-core/custom-backend-integration/SKILL.md +86 -52
  43. package/skills/ai-core/debug-logging/SKILL.md +100 -18
  44. package/skills/ai-core/locks/SKILL.md +35 -7
  45. package/skills/ai-core/media-generation/SKILL.md +114 -49
  46. package/skills/ai-core/middleware/SKILL.md +174 -69
  47. package/skills/ai-core/structured-outputs/SKILL.md +98 -49
  48. package/skills/ai-core/tool-calling/SKILL.md +245 -158
  49. package/src/activities/chat/index.ts +6 -7
  50. package/src/activities/generateLiveVideo/adapter.ts +99 -0
  51. package/src/activities/generateLiveVideo/index.ts +339 -0
  52. package/src/activities/generateVideo/index.ts +3 -4
  53. package/src/activities/generateWorld/adapter.ts +96 -0
  54. package/src/activities/generateWorld/index.ts +339 -0
  55. package/src/activities/index.ts +44 -0
  56. package/src/activities/middleware/types.ts +2 -0
  57. package/src/client.ts +8 -0
  58. package/src/index.ts +8 -0
  59. package/src/middlewares/otel.ts +2 -0
  60. 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 { getProductsDef } from './definitions'
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 = await db.products.search(query, { limit: limit ?? 10 })
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
- ```typescript
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 "@tanstack/ai-react";
96
- import { updateCartUIDef } from "@/tools/definitions";
97
- import { useState } from "react";
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("/api/chat"),
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 === "text") return <p>{part.content}</p>;
123
- if (part.type === "tool-call") {
124
- return <div key={part.id}>Tool: {part.name} ({part.state})</div>;
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
- const stream = chat({
214
- adapter: openaiText('gpt-5.5'),
215
- messages,
216
- tools: [getUserData],
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
- const stream = chat({
244
- adapter: openaiText('gpt-5.5'),
245
- messages,
246
- tools: [showNotificationDef],
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
- ```typescript
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 "@tanstack/ai-react";
259
- import { showNotificationDef } from "@/tools/definitions";
260
- import { useState } from "react";
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("/api/chat"),
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 === "text" ? <p>{part.content}</p> : null
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
- ```typescript
327
- import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
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("/api/chat"),
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 !== "tool-approval") return null;
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 === "text" ? <p key={part.content}>{part.content}</p> : null
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
- ```typescript
368
- // Payloadless tool-approvals only
369
- resolveInterrupts(true)
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
- // Or per-item:
372
- resolveInterrupts((interrupt) => {
373
- if (interrupt.kind === 'tool-approval') {
374
- interrupt.resolveInterrupt(true)
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
- const stream = chat({
445
- adapter: openaiText('gpt-5.5'),
446
- messages,
447
- tools: [getProducts, compareProducts],
448
- agentLoopStrategy: maxIterations(20),
449
- lazyToolsConfig: { includeDescription: 'first-sentence' },
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
- // src/routes/api.chat.ts
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 const Route = createFileRoute('/api/chat')({
485
- server: {
486
- handlers: {
487
- POST: async ({ request }) => {
488
- const { messages } = await request.json()
489
-
490
- // 1. Connect to the MCP server.
491
- const mcp = await createMCPClient({
492
- transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
493
- })
494
-
495
- // 2. Discover all tools from the server (returns ServerTool[]).
496
- const mcpTools = await mcp.tools()
497
-
498
- // 3. Spread them into chat() — they work exactly like hand-written tools.
499
- // Caller owns the lifecycle — chat() never closes the client. Tools run
500
- // while the response streams, so close in a middleware terminal hook
501
- // (a try/finally around the return would close before tools execute).
502
- const stream = chat({
503
- adapter: openaiText('gpt-5.5'),
504
- messages,
505
- tools: [...mcpTools],
506
- middleware: [
507
- {
508
- name: 'mcp-close',
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
- const longRunningTool = myToolDef.server(async (args, ctx) => {
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('https://slow.api/data', {
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
- import { createFileRoute } from '@tanstack/react-router'
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 const Route = createFileRoute('/api/chat')({
648
- server: {
649
- handlers: {
650
- POST: async ({ request }) => {
651
- const { messages } = await request.json()
652
-
653
- const mcpClient = await createMCPClient({
654
- transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
655
- })
656
-
657
- const stream = chat({
658
- adapter: openaiText('gpt-5.5'),
659
- messages,
660
- mcp: {
661
- clients: [mcpClient],
662
- connection: 'keep-alive',
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
- ```typescript
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
- useChat({ connection: fetchServerSentEvents('/api/chat') }) // no tools
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
- ```typescript
774
- chat({ adapter, messages }); // no tools
775
- useChat({ ..., tools: clientTools(myToolDef.client(() => result)) });
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
- ```typescript
781
- chat({ adapter, messages, tools: [myToolDef] });
782
- useChat({ ..., tools: clientTools(myToolDef.client((input) => ({ success: true }))) });
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