@tanstack/ai 0.27.0 → 0.28.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/dist/esm/activities/chat/index.d.ts +7 -0
- package/dist/esm/activities/chat/index.js +76 -10
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/mcp/manager.d.ts +25 -0
- package/dist/esm/activities/chat/mcp/manager.js +71 -0
- package/dist/esm/activities/chat/mcp/manager.js.map +1 -0
- package/dist/esm/activities/chat/mcp/types.d.ts +56 -0
- package/dist/esm/activities/chat/stream/message-updaters.js +20 -8
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.js +2 -1
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/extend-adapter.d.ts +22 -6
- package/dist/esm/extend-adapter.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types.d.ts +6 -0
- package/package.json +2 -2
- package/skills/ai-core/chat-experience/SKILL.md +71 -0
- package/skills/ai-core/tool-calling/SKILL.md +287 -0
- package/src/activities/chat/index.ts +95 -12
- package/src/activities/chat/mcp/manager.ts +85 -0
- package/src/activities/chat/mcp/types.ts +66 -0
- package/src/activities/chat/stream/message-updaters.ts +22 -9
- package/src/activities/chat/tools/tool-calls.ts +2 -0
- package/src/extend-adapter.ts +42 -24
- package/src/index.ts +10 -0
- package/src/types.ts +6 -0
package/dist/esm/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export { createSpeechOptions } from './activities/generateSpeech/index.js';
|
|
|
8
8
|
export { createTranscriptionOptions } from './activities/generateTranscription/index.js';
|
|
9
9
|
export type { AIAdapter, ImageAdapter, AnyImageAdapter, TextAdapter, AnyTextAdapter, AnySummarizeAdapter, SummarizeAdapter, AnyAudioAdapter, AudioAdapter, AnyTTSAdapter, TTSAdapter, AnyTranscriptionAdapter, TranscriptionAdapter, AnyVideoAdapter, VideoAdapter, } from './activities/index.js';
|
|
10
10
|
export { toolDefinition, type ToolDefinition, type ToolDefinitionInstance, type ToolDefinitionConfig, type ServerTool, type ClientTool, type AnyClientTool, type InferToolName, type InferToolInput, type InferToolOutput, } from './activities/chat/tools/tool-definition.js';
|
|
11
|
+
export type { MCPToolSource, ChatMCPOptions, MCPConnectionPolicy, } from './activities/chat/mcp/types.js';
|
|
12
|
+
export { MCPDuplicateToolNameError } from './activities/chat/mcp/manager.js';
|
|
11
13
|
export { convertSchemaToJsonSchema, isStandardSchema, parseWithStandardSchema, StandardSchemaValidationError, } from './activities/chat/tools/schema-converter.js';
|
|
12
14
|
export { streamToText, toServerSentEventsStream, toServerSentEventsResponse, toHttpStream, toHttpResponse, } from './stream-to-response.js';
|
|
13
15
|
export { ToolCallManager } from './activities/chat/tools/tool-calls.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createVideoOptions, generateVideo, getVideoJobStatus } from "./activiti
|
|
|
6
6
|
import { createSpeechOptions, generateSpeech } from "./activities/generateSpeech/index.js";
|
|
7
7
|
import { createTranscriptionOptions, generateTranscription } from "./activities/generateTranscription/index.js";
|
|
8
8
|
import { toolDefinition } from "./activities/chat/tools/tool-definition.js";
|
|
9
|
+
import { MCPDuplicateToolNameError } from "./activities/chat/mcp/manager.js";
|
|
9
10
|
import { StandardSchemaValidationError, convertSchemaToJsonSchema, isStandardSchema, parseWithStandardSchema } from "./activities/chat/tools/schema-converter.js";
|
|
10
11
|
import { streamToText, toHttpResponse, toHttpStream, toServerSentEventsResponse, toServerSentEventsStream } from "./stream-to-response.js";
|
|
11
12
|
import { ToolCallManager } from "./activities/chat/tools/tool-calls.js";
|
|
@@ -32,6 +33,7 @@ export {
|
|
|
32
33
|
ConsoleLogger,
|
|
33
34
|
EventType,
|
|
34
35
|
ImmediateStrategy,
|
|
36
|
+
MCPDuplicateToolNameError,
|
|
35
37
|
PartialJSONParser,
|
|
36
38
|
PunctuationStrategy,
|
|
37
39
|
StandardSchemaValidationError,
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -348,6 +348,12 @@ type RuntimeContextField<TContext> = IsUnknown<TContext> extends true ? {
|
|
|
348
348
|
export type ToolExecutionContext<TContext = unknown> = RuntimeContextField<TContext> & {
|
|
349
349
|
/** The ID of the tool call being executed */
|
|
350
350
|
toolCallId?: string;
|
|
351
|
+
/**
|
|
352
|
+
* Abort signal for the current chat run. Aborts when the run's
|
|
353
|
+
* `abortController` fires (or middleware aborts). Long-running tools —
|
|
354
|
+
* e.g. MCP `callTool` — should forward this to cancel in-flight work.
|
|
355
|
+
*/
|
|
356
|
+
abortSignal?: AbortSignal;
|
|
351
357
|
/**
|
|
352
358
|
* Emit a custom event during tool execution.
|
|
353
359
|
* Events are streamed to the client in real-time as AG-UI CUSTOM events.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"@ag-ui/core": "^0.0.52",
|
|
69
69
|
"@standard-schema/spec": "^1.1.0",
|
|
70
70
|
"partial-json": "^0.1.7",
|
|
71
|
-
"@tanstack/ai-event-client": "0.5.
|
|
71
|
+
"@tanstack/ai-event-client": "0.5.4"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"@opentelemetry/api": ">=1.9.0"
|
|
@@ -310,6 +310,77 @@ const { messages, sendMessage } = useChat({
|
|
|
310
310
|
The only difference is swapping `toServerSentEventsResponse` / `fetchServerSentEvents`
|
|
311
311
|
for `toHttpResponse` / `fetchHttpStream`. Everything else stays identical.
|
|
312
312
|
|
|
313
|
+
### 5. MCP Tool Discovery via `chat({ mcp })`
|
|
314
|
+
|
|
315
|
+
Pass `mcp` to let `chat()` own discovery **and** lifecycle for one or more MCP
|
|
316
|
+
clients. Useful when you want minimal boilerplate and don't need to reuse the
|
|
317
|
+
clients across calls.
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// Prop shape:
|
|
321
|
+
// chat({
|
|
322
|
+
// ...,
|
|
323
|
+
// mcp: {
|
|
324
|
+
// clients: Array<MCPClient | MCPClients>,
|
|
325
|
+
// connection?: 'close' | 'keep-alive', // default: 'close'
|
|
326
|
+
// lazyTools?: boolean,
|
|
327
|
+
// onDiscoveryError?: (error: unknown, source) => void,
|
|
328
|
+
// }
|
|
329
|
+
// })
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
- **`clients`** — one or more `MCPClient` / `MCPClients` instances.
|
|
333
|
+
- **`connection`** — `'close'` (default) closes each client when the run ends
|
|
334
|
+
(after the agent loop completes and the stream is drained); with
|
|
335
|
+
`'keep-alive'`, `chat()` never closes the clients — the caller owns their
|
|
336
|
+
lifecycle (keep connections warm across requests).
|
|
337
|
+
- **`lazyTools`** — forwarded to `tools({ lazy: true })` so tool schemas are
|
|
338
|
+
sent to the LLM on demand.
|
|
339
|
+
- **`onDiscoveryError`** — throw (or re-throw) to fail the entire call fast;
|
|
340
|
+
return normally to skip that source and continue. Omit to rethrow (fail-fast).
|
|
341
|
+
|
|
342
|
+
**When to use `mcp` vs. the tools spread:**
|
|
343
|
+
|
|
344
|
+
| Approach | Use when |
|
|
345
|
+
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
346
|
+
| `chat({ mcp: { clients: [...] } })` | You want discovery + lifecycle managed for you, and don't need fully-typed input/output schemas |
|
|
347
|
+
| `tools: [...await client.tools([toolDefinition(...)])]` | You want fully-typed MCP tools with Zod input/output validation |
|
|
348
|
+
|
|
349
|
+
**Server-side example:**
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
353
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
354
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
355
|
+
import { createMCPClient } from '@tanstack/ai-mcp'
|
|
356
|
+
|
|
357
|
+
export const Route = createFileRoute('/api/chat')({
|
|
358
|
+
server: {
|
|
359
|
+
handlers: {
|
|
360
|
+
POST: async ({ request }) => {
|
|
361
|
+
const { messages } = await request.json()
|
|
362
|
+
|
|
363
|
+
const mcpClient = await createMCPClient({
|
|
364
|
+
transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
const stream = chat({
|
|
368
|
+
adapter: openaiText('gpt-5.5'),
|
|
369
|
+
messages,
|
|
370
|
+
mcp: {
|
|
371
|
+
clients: [mcpClient],
|
|
372
|
+
connection: 'keep-alive', // chat() won't close it — reuse across requests
|
|
373
|
+
},
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
return toServerSentEventsResponse(stream)
|
|
377
|
+
// connection: 'keep-alive' — chat() never closes mcpClient; it stays open for reuse across runs.
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
})
|
|
382
|
+
```
|
|
383
|
+
|
|
313
384
|
## Common Mistakes
|
|
314
385
|
|
|
315
386
|
### a. CRITICAL: Using Vercel AI SDK patterns (streamText, generateText)
|
|
@@ -375,6 +375,293 @@ gets the full schema, then calls `compareProducts` directly.
|
|
|
375
375
|
Once discovered, a tool stays available for the conversation.
|
|
376
376
|
When all lazy tools are discovered, the discovery tool is removed automatically.
|
|
377
377
|
|
|
378
|
+
## MCP Tools
|
|
379
|
+
|
|
380
|
+
`@tanstack/ai-mcp` lets a server-side `chat()` call discover and invoke tools
|
|
381
|
+
hosted on any MCP server (Streamable HTTP, SSE, or stdio).
|
|
382
|
+
|
|
383
|
+
### Basic usage — auto-discovery
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
// src/routes/api.chat.ts
|
|
387
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
388
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
389
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
390
|
+
import { createMCPClient } from '@tanstack/ai-mcp'
|
|
391
|
+
|
|
392
|
+
export const Route = createFileRoute('/api/chat')({
|
|
393
|
+
server: {
|
|
394
|
+
handlers: {
|
|
395
|
+
POST: async ({ request }) => {
|
|
396
|
+
const { messages } = await request.json()
|
|
397
|
+
|
|
398
|
+
// 1. Connect to the MCP server.
|
|
399
|
+
const mcp = await createMCPClient({
|
|
400
|
+
transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
// 2. Discover all tools from the server (returns ServerTool[]).
|
|
404
|
+
const mcpTools = await mcp.tools()
|
|
405
|
+
|
|
406
|
+
// 3. Spread them into chat() — they work exactly like hand-written tools.
|
|
407
|
+
// Caller owns the lifecycle — chat() never closes the client. Tools run
|
|
408
|
+
// while the response streams, so close in a middleware terminal hook
|
|
409
|
+
// (a try/finally around the return would close before tools execute).
|
|
410
|
+
const stream = chat({
|
|
411
|
+
adapter: openaiText('gpt-5.5'),
|
|
412
|
+
messages,
|
|
413
|
+
tools: [...mcpTools],
|
|
414
|
+
middleware: [
|
|
415
|
+
{
|
|
416
|
+
name: 'mcp-close',
|
|
417
|
+
onFinish: () => mcp.close(),
|
|
418
|
+
onAbort: () => mcp.close(),
|
|
419
|
+
onError: () => mcp.close(),
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
})
|
|
423
|
+
return toServerSentEventsResponse(stream)
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
})
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Typed path — pass toolDefinition instances
|
|
431
|
+
|
|
432
|
+
Pass bare `toolDefinition()` instances (no `.server()`) to `client.tools([...])`.
|
|
433
|
+
The MCP client supplies a `callTool` proxy as the execute function, while
|
|
434
|
+
input/output validation and types come from the definitions' Zod schemas.
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
import { toolDefinition } from '@tanstack/ai'
|
|
438
|
+
import { createMCPClient } from '@tanstack/ai-mcp'
|
|
439
|
+
import { z } from 'zod'
|
|
440
|
+
|
|
441
|
+
const getWeather = toolDefinition({
|
|
442
|
+
name: 'get_weather',
|
|
443
|
+
description: 'Current weather for a city',
|
|
444
|
+
inputSchema: z.object({ city: z.string() }),
|
|
445
|
+
outputSchema: z.object({ temperature: z.number(), conditions: z.string() }),
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
const mcp = await createMCPClient({
|
|
449
|
+
transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
// Returns ServerTool[] typed to the definitions' input/output schemas.
|
|
453
|
+
// Throws MCPToolNotFoundError if the server does not expose a tool with that name.
|
|
454
|
+
const tools = await mcp.tools([getWeather])
|
|
455
|
+
|
|
456
|
+
const stream = chat({ adapter: openaiText('gpt-5.5'), messages, tools })
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Multiple servers with `createMCPClients`
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { createMCPClients } from '@tanstack/ai-mcp'
|
|
463
|
+
|
|
464
|
+
// Each key becomes the default prefix for that server's tools.
|
|
465
|
+
await using pool = await createMCPClients({
|
|
466
|
+
github: { transport: { type: 'http', url: 'https://mcp.github.com/mcp' } },
|
|
467
|
+
linear: { transport: { type: 'http', url: 'https://mcp.linear.app/mcp' } },
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
// Tools auto-prefixed: 'github_search_repos', 'linear_create_issue', etc.
|
|
471
|
+
const tools = await pool.tools()
|
|
472
|
+
|
|
473
|
+
const stream = chat({ adapter: openaiText('gpt-5.5'), messages, tools })
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Use `pool.clients.<name>` for typed per-server access (resources, prompts, typed
|
|
477
|
+
`tools([defs])` overload).
|
|
478
|
+
|
|
479
|
+
### `ToolExecutionContext.abortSignal` — cancelling long-running tools
|
|
480
|
+
|
|
481
|
+
Every server tool's execute function now receives `abortSignal` in its context.
|
|
482
|
+
When the chat run aborts (e.g. the client disconnects or calls the run's
|
|
483
|
+
`abortController`), the signal fires and any in-flight `callTool` call is
|
|
484
|
+
cancelled automatically.
|
|
485
|
+
|
|
486
|
+
You can also forward it from your own server tools:
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
const longRunningTool = myToolDef.server(async (args, ctx) => {
|
|
490
|
+
// Forward to fetch, a DB query, or an MCP callTool call.
|
|
491
|
+
const response = await fetch('https://slow.api/data', {
|
|
492
|
+
signal: ctx?.abortSignal,
|
|
493
|
+
})
|
|
494
|
+
return response.json()
|
|
495
|
+
})
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
MCP tools wire this automatically — `makeMcpExecute` passes `ctx?.abortSignal`
|
|
499
|
+
as the `signal` option to `client.callTool(...)`, so MCP server calls cancel
|
|
500
|
+
with the chat run without any extra code.
|
|
501
|
+
|
|
502
|
+
### stdio transport (Node-only)
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
import { createMCPClient } from '@tanstack/ai-mcp'
|
|
506
|
+
import { stdioTransport } from '@tanstack/ai-mcp/stdio'
|
|
507
|
+
|
|
508
|
+
const mcp = await createMCPClient({
|
|
509
|
+
transport: stdioTransport({ command: 'npx', args: ['-y', 'my-mcp-server'] }),
|
|
510
|
+
})
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Import `stdioTransport` from the `/stdio` subpath only — it contains Node.js
|
|
514
|
+
`child_process` imports and must not be bundled for edge runtimes.
|
|
515
|
+
|
|
516
|
+
### `chat({ mcp })` — discovery + lifecycle in one prop
|
|
517
|
+
|
|
518
|
+
Instead of manually calling `client.tools()` and managing `close()`, pass an
|
|
519
|
+
`mcp` object and let `chat()` handle discovery and lifecycle.
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
// Prop shape (ChatMCPOptions):
|
|
523
|
+
// mcp: {
|
|
524
|
+
// clients: Array<MCPClient | MCPClients>,
|
|
525
|
+
// connection?: 'close' | 'keep-alive', // default: 'close'
|
|
526
|
+
// lazyTools?: boolean,
|
|
527
|
+
// onDiscoveryError?: (error: unknown, source) => void,
|
|
528
|
+
// }
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
- At run start, `chat()` calls `.tools()` on every entry in `clients` and merges
|
|
532
|
+
the results — identical to spreading `await client.tools()` into `tools: [...]`.
|
|
533
|
+
- `lazyTools: true` is forwarded to `tools({ lazy: true })`.
|
|
534
|
+
- `onDiscoveryError`: throw to fail-fast; return to skip that source.
|
|
535
|
+
- `connection: 'close'` (default) closes each client when the run ends (after
|
|
536
|
+
the agent loop completes and the stream is drained). With `'keep-alive'`,
|
|
537
|
+
`chat()` never closes the clients — the caller owns their lifecycle (keep
|
|
538
|
+
connections warm across requests).
|
|
539
|
+
|
|
540
|
+
**When to use `mcp` vs. the tools spread:**
|
|
541
|
+
|
|
542
|
+
| Approach | Use when |
|
|
543
|
+
| ------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
544
|
+
| `chat({ mcp: { clients: [...] } })` | Convenience: discovery + lifecycle in one place; untyped tool args are acceptable |
|
|
545
|
+
| `tools: [...await client.tools([toolDefinition(...)])]` | Fully-typed tool args/results via Zod schemas |
|
|
546
|
+
|
|
547
|
+
**Example:**
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
551
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
552
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
553
|
+
import { createMCPClient } from '@tanstack/ai-mcp'
|
|
554
|
+
|
|
555
|
+
export const Route = createFileRoute('/api/chat')({
|
|
556
|
+
server: {
|
|
557
|
+
handlers: {
|
|
558
|
+
POST: async ({ request }) => {
|
|
559
|
+
const { messages } = await request.json()
|
|
560
|
+
|
|
561
|
+
const mcpClient = await createMCPClient({
|
|
562
|
+
transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
const stream = chat({
|
|
566
|
+
adapter: openaiText('gpt-5.5'),
|
|
567
|
+
messages,
|
|
568
|
+
mcp: {
|
|
569
|
+
clients: [mcpClient],
|
|
570
|
+
connection: 'keep-alive',
|
|
571
|
+
onDiscoveryError: (err, source) => {
|
|
572
|
+
console.warn('MCP discovery failed, skipping source:', err)
|
|
573
|
+
// returning (not throwing) skips this source and continues
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
return toServerSentEventsResponse(stream)
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
})
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
## Provider Skills
|
|
586
|
+
|
|
587
|
+
> **Not to be confused with `@tanstack/ai-code-mode-skills`**, which are locally-generated TypeScript functions executed client-side. Provider Skills are hosted, provider-managed bundles that the model loads on demand and runs inside the provider's server-side sandbox.
|
|
588
|
+
|
|
589
|
+
Provider Skills are inert without an execution tool. The execution tool is what activates the sandbox; skills are additional capability bundles that run inside it:
|
|
590
|
+
|
|
591
|
+
- **Anthropic**: skills require the `code_execution` tool (`@tanstack/ai-anthropic/tools`).
|
|
592
|
+
- **OpenAI**: skills live inside the `shell` tool (`@tanstack/ai-openai/tools`) and are Responses API only.
|
|
593
|
+
|
|
594
|
+
### Anthropic: `codeExecutionTool` with skills
|
|
595
|
+
|
|
596
|
+
Import from `@tanstack/ai-anthropic/tools`:
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
import { codeExecutionTool } from '@tanstack/ai-anthropic/tools'
|
|
600
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
601
|
+
import { anthropicText } from '@tanstack/ai-anthropic'
|
|
602
|
+
|
|
603
|
+
export async function POST(request: Request) {
|
|
604
|
+
const { messages } = await request.json()
|
|
605
|
+
const stream = chat({
|
|
606
|
+
adapter: anthropicText('claude-sonnet-4-5'),
|
|
607
|
+
messages,
|
|
608
|
+
tools: [
|
|
609
|
+
codeExecutionTool(
|
|
610
|
+
{ type: 'code_execution_20250825', name: 'code_execution' },
|
|
611
|
+
{
|
|
612
|
+
skills: [{ type: 'anthropic', skill_id: 'pptx', version: 'latest' }],
|
|
613
|
+
},
|
|
614
|
+
),
|
|
615
|
+
],
|
|
616
|
+
})
|
|
617
|
+
return toServerSentEventsResponse(stream)
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
`AnthropicContainerSkill` shape: `{ type: 'anthropic' | 'custom'; skill_id: string; version?: string }`. Constraints: max 8 skills per request; `skill_id` must be 1–64 characters.
|
|
622
|
+
|
|
623
|
+
The adapter automatically:
|
|
624
|
+
|
|
625
|
+
- Lifts the skills into the request's top-level `container.skills` param (the shape Anthropic's API requires).
|
|
626
|
+
- Attaches the required beta headers (`code-execution-2025-08-25` plus `skills-2025-10-02` when skills are present). You do not set these manually.
|
|
627
|
+
|
|
628
|
+
**Deprecation:** Setting skills via `modelOptions.container.skills` is deprecated. Use `codeExecutionTool(config, { skills })` instead — the legacy path bypasses the beta-header wiring.
|
|
629
|
+
|
|
630
|
+
### OpenAI: `shellTool` with skills (Responses API only)
|
|
631
|
+
|
|
632
|
+
Import from `@tanstack/ai-openai/tools`:
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
import { shellTool } from '@tanstack/ai-openai/tools'
|
|
636
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
637
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
638
|
+
|
|
639
|
+
export async function POST(request: Request) {
|
|
640
|
+
const { messages } = await request.json()
|
|
641
|
+
const stream = chat({
|
|
642
|
+
adapter: openaiText('gpt-5.2'),
|
|
643
|
+
messages,
|
|
644
|
+
tools: [
|
|
645
|
+
shellTool({
|
|
646
|
+
environment: {
|
|
647
|
+
type: 'container_auto',
|
|
648
|
+
skills: [
|
|
649
|
+
{ type: 'skill_reference', skill_id: 'skill_abc', version: '2' },
|
|
650
|
+
],
|
|
651
|
+
},
|
|
652
|
+
}),
|
|
653
|
+
],
|
|
654
|
+
})
|
|
655
|
+
return toServerSentEventsResponse(stream)
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
`SkillReference` shape: `{ type: 'skill_reference'; skill_id: string; version?: string }`. `version` is a string — use a positive integer as a string (e.g. `'2'`) or `'latest'`. This is Responses API only; Chat Completions does not support the shell tool.
|
|
660
|
+
|
|
661
|
+
### Scope
|
|
662
|
+
|
|
663
|
+
Only hosted/managed-by-id skills (`type: 'anthropic'` / `type: 'custom'` for Anthropic; `type: 'skill_reference'` for OpenAI) are wired. Inline bundles, local-path, and upload-API skill creation are not handled by these factories.
|
|
664
|
+
|
|
378
665
|
## Common Mistakes
|
|
379
666
|
|
|
380
667
|
### a. HIGH: Not passing tool definitions to both server and client
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { maxIterations as maxIterationsStrategy } from './agent-loop-strategies'
|
|
26
26
|
import { convertMessagesToModelMessages, generateMessageId } from './messages'
|
|
27
27
|
import { MiddlewareRunner } from './middleware/compose'
|
|
28
|
+
import { MCPManager } from './mcp/manager'
|
|
28
29
|
import type {
|
|
29
30
|
ApprovalRequest,
|
|
30
31
|
ClientToolRequest,
|
|
@@ -69,6 +70,7 @@ import type {
|
|
|
69
70
|
MergeContext,
|
|
70
71
|
UnionToIntersection,
|
|
71
72
|
} from './runtime-context-types'
|
|
73
|
+
import type { ChatMCPOptions } from './mcp/types'
|
|
72
74
|
|
|
73
75
|
// ===========================
|
|
74
76
|
// Activity Kind
|
|
@@ -208,6 +210,12 @@ export interface TextActivityOptions<
|
|
|
208
210
|
| ProviderTool<string, TAdapter['~types']['toolCapabilities'][number]>
|
|
209
211
|
>
|
|
210
212
|
| undefined
|
|
213
|
+
/**
|
|
214
|
+
* Hand MCP clients/pools to chat(): their tools are discovered at run start
|
|
215
|
+
* and merged into the run; `connection` controls whether chat() closes them
|
|
216
|
+
* when the run ends. See docs/tools/mcp.md "Managing MCP clients with chat()".
|
|
217
|
+
*/
|
|
218
|
+
mcp?: ChatMCPOptions
|
|
211
219
|
/** Additional metadata to attach to the request. */
|
|
212
220
|
metadata?: TextOptions['metadata']
|
|
213
221
|
/** Model-specific provider options (type comes from adapter) */
|
|
@@ -432,6 +440,28 @@ interface TextEngineConfig<
|
|
|
432
440
|
type ToolPhaseResult = 'continue' | 'stop' | 'wait'
|
|
433
441
|
type CyclePhase = 'processText' | 'executeToolCalls'
|
|
434
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Combine two optional AbortSignals into one that aborts when either does.
|
|
445
|
+
* Returns the other signal directly when one is absent or already aborted.
|
|
446
|
+
* (Manual implementation — `AbortSignal.any` requires Node >= 20.3.)
|
|
447
|
+
*/
|
|
448
|
+
function combineAbortSignals(
|
|
449
|
+
a: AbortSignal | undefined,
|
|
450
|
+
b: AbortSignal | undefined,
|
|
451
|
+
): AbortSignal | undefined {
|
|
452
|
+
if (!a) return b
|
|
453
|
+
if (!b) return a
|
|
454
|
+
if (a.aborted) return a
|
|
455
|
+
if (b.aborted) return b
|
|
456
|
+
const controller = new AbortController()
|
|
457
|
+
const onAbort = (source: AbortSignal) => () => {
|
|
458
|
+
controller.abort(source.reason)
|
|
459
|
+
}
|
|
460
|
+
a.addEventListener('abort', onAbort(a), { once: true })
|
|
461
|
+
b.addEventListener('abort', onAbort(b), { once: true })
|
|
462
|
+
return controller.signal
|
|
463
|
+
}
|
|
464
|
+
|
|
435
465
|
class TextEngine<
|
|
436
466
|
TAdapter extends AnyTextAdapter,
|
|
437
467
|
TContext = unknown,
|
|
@@ -486,6 +516,9 @@ class TextEngine<
|
|
|
486
516
|
private readonly deferredPromises: Array<Promise<unknown>> = []
|
|
487
517
|
private abortReason?: string
|
|
488
518
|
private readonly middlewareAbortController?: AbortController
|
|
519
|
+
// Combines the caller's signal with middleware abort() so running tools
|
|
520
|
+
// observe both cancellation sources via ctx.abortSignal.
|
|
521
|
+
private readonly toolAbortSignal?: AbortSignal
|
|
489
522
|
private terminalHookCalled = false
|
|
490
523
|
|
|
491
524
|
private readonly logger: InternalLogger
|
|
@@ -583,6 +616,10 @@ class TextEngine<
|
|
|
583
616
|
]
|
|
584
617
|
this.middlewareRunner = new MiddlewareRunner(allMiddleware, logger)
|
|
585
618
|
this.middlewareAbortController = new AbortController()
|
|
619
|
+
this.toolAbortSignal = combineAbortSignals(
|
|
620
|
+
this.effectiveSignal,
|
|
621
|
+
this.middlewareAbortController.signal,
|
|
622
|
+
)
|
|
586
623
|
this.middlewareCtx = {
|
|
587
624
|
requestId: this.requestId,
|
|
588
625
|
streamId: this.streamId,
|
|
@@ -1225,6 +1262,7 @@ class TextEngine<
|
|
|
1225
1262
|
},
|
|
1226
1263
|
},
|
|
1227
1264
|
this.middlewareCtx.context,
|
|
1265
|
+
this.toolAbortSignal,
|
|
1228
1266
|
)
|
|
1229
1267
|
|
|
1230
1268
|
// Consume the async generator, yielding custom events and collecting the return value
|
|
@@ -1386,6 +1424,7 @@ class TextEngine<
|
|
|
1386
1424
|
},
|
|
1387
1425
|
},
|
|
1388
1426
|
this.middlewareCtx.context,
|
|
1427
|
+
this.toolAbortSignal,
|
|
1389
1428
|
)
|
|
1390
1429
|
|
|
1391
1430
|
// Consume the async generator, yielding custom events and collecting the return value
|
|
@@ -2568,10 +2607,16 @@ export function chat<
|
|
|
2568
2607
|
async function* runStreamingText<TContext = unknown>(
|
|
2569
2608
|
options: TextActivityOptions<AnyTextAdapter, undefined, true, TContext>,
|
|
2570
2609
|
): AsyncIterable<StreamChunk> {
|
|
2571
|
-
const { adapter, middleware, context, debug, ...textOptions } = options
|
|
2610
|
+
const { adapter, middleware, context, debug, mcp, ...textOptions } = options
|
|
2572
2611
|
const model = adapter.model
|
|
2573
2612
|
const logger = resolveDebugOption(debug)
|
|
2574
2613
|
|
|
2614
|
+
const mcpManager = MCPManager.from(mcp)
|
|
2615
|
+
const mcpTools = await mcpManager.discover()
|
|
2616
|
+
if (mcpTools.length > 0) {
|
|
2617
|
+
textOptions.tools = [...(textOptions.tools ?? []), ...mcpTools]
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2575
2620
|
const engine = new TextEngine(
|
|
2576
2621
|
{
|
|
2577
2622
|
adapter,
|
|
@@ -2586,8 +2631,12 @@ async function* runStreamingText<TContext = unknown>(
|
|
|
2586
2631
|
logger,
|
|
2587
2632
|
)
|
|
2588
2633
|
|
|
2589
|
-
|
|
2590
|
-
|
|
2634
|
+
try {
|
|
2635
|
+
for await (const chunk of engine.run()) {
|
|
2636
|
+
yield chunk
|
|
2637
|
+
}
|
|
2638
|
+
} finally {
|
|
2639
|
+
await mcpManager.dispose()
|
|
2591
2640
|
}
|
|
2592
2641
|
}
|
|
2593
2642
|
|
|
@@ -2624,8 +2673,15 @@ async function runAgenticStructuredOutput<
|
|
|
2624
2673
|
>(
|
|
2625
2674
|
options: TextActivityOptions<AnyTextAdapter, TSchema, boolean, TContext>,
|
|
2626
2675
|
): Promise<InferSchemaType<TSchema>> {
|
|
2627
|
-
const {
|
|
2628
|
-
|
|
2676
|
+
const {
|
|
2677
|
+
adapter,
|
|
2678
|
+
outputSchema,
|
|
2679
|
+
middleware,
|
|
2680
|
+
context,
|
|
2681
|
+
debug,
|
|
2682
|
+
mcp,
|
|
2683
|
+
...textOptions
|
|
2684
|
+
} = options
|
|
2629
2685
|
const model = adapter.model
|
|
2630
2686
|
const logger = resolveDebugOption(debug)
|
|
2631
2687
|
|
|
@@ -2659,6 +2715,12 @@ async function runAgenticStructuredOutput<
|
|
|
2659
2715
|
const nativeCombined =
|
|
2660
2716
|
adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true
|
|
2661
2717
|
|
|
2718
|
+
const mcpManager = MCPManager.from(mcp)
|
|
2719
|
+
const mcpTools = await mcpManager.discover()
|
|
2720
|
+
if (mcpTools.length > 0) {
|
|
2721
|
+
textOptions.tools = [...(textOptions.tools ?? []), ...mcpTools]
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2662
2724
|
const engine = new TextEngine(
|
|
2663
2725
|
{
|
|
2664
2726
|
adapter,
|
|
@@ -2679,9 +2741,13 @@ async function runAgenticStructuredOutput<
|
|
|
2679
2741
|
logger,
|
|
2680
2742
|
)
|
|
2681
2743
|
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2744
|
+
try {
|
|
2745
|
+
// Consume the stream — chunks pipe through middleware but are not yielded externally
|
|
2746
|
+
for await (const _chunk of engine.run()) {
|
|
2747
|
+
// intentionally empty
|
|
2748
|
+
}
|
|
2749
|
+
} finally {
|
|
2750
|
+
await mcpManager.dispose()
|
|
2685
2751
|
}
|
|
2686
2752
|
|
|
2687
2753
|
const finalizationError = engine.getFinalizationError()
|
|
@@ -2912,8 +2978,15 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
2912
2978
|
options: TextActivityOptions<AnyTextAdapter, TSchema, true, TContext>,
|
|
2913
2979
|
jsonSchema: NonNullable<ReturnType<typeof convertSchemaToJsonSchema>>,
|
|
2914
2980
|
): StructuredOutputStreamInternal<InferSchemaType<TSchema>> {
|
|
2915
|
-
const {
|
|
2916
|
-
|
|
2981
|
+
const {
|
|
2982
|
+
adapter,
|
|
2983
|
+
outputSchema,
|
|
2984
|
+
middleware,
|
|
2985
|
+
context,
|
|
2986
|
+
debug,
|
|
2987
|
+
mcp,
|
|
2988
|
+
...textOptions
|
|
2989
|
+
} = options
|
|
2917
2990
|
const model = adapter.model
|
|
2918
2991
|
const logger = resolveDebugOption(debug)
|
|
2919
2992
|
|
|
@@ -2927,6 +3000,12 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
2927
3000
|
const nativeCombined =
|
|
2928
3001
|
adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true
|
|
2929
3002
|
|
|
3003
|
+
const mcpManager = MCPManager.from(mcp)
|
|
3004
|
+
const mcpTools = await mcpManager.discover()
|
|
3005
|
+
if (mcpTools.length > 0) {
|
|
3006
|
+
textOptions.tools = [...(textOptions.tools ?? []), ...mcpTools]
|
|
3007
|
+
}
|
|
3008
|
+
|
|
2930
3009
|
// Inputs may be UIMessages (from useChat) or ModelMessages (from server-side
|
|
2931
3010
|
// callers). TextEngine handles the conversion uniformly.
|
|
2932
3011
|
const engine = new TextEngine(
|
|
@@ -2948,8 +3027,12 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
2948
3027
|
logger,
|
|
2949
3028
|
)
|
|
2950
3029
|
|
|
2951
|
-
|
|
2952
|
-
|
|
3030
|
+
try {
|
|
3031
|
+
for await (const chunk of engine.run()) {
|
|
3032
|
+
yield chunk
|
|
3033
|
+
}
|
|
3034
|
+
} finally {
|
|
3035
|
+
await mcpManager.dispose()
|
|
2953
3036
|
}
|
|
2954
3037
|
|
|
2955
3038
|
// Schema validation for the streaming variant remains the consumer's
|