@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.
@@ -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,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -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.27.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.3"
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
- for await (const chunk of engine.run()) {
2590
- yield chunk
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 { adapter, outputSchema, middleware, context, debug, ...textOptions } =
2628
- options
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
- // Consume the stream — chunks pipe through middleware but are not yielded externally
2683
- for await (const _chunk of engine.run()) {
2684
- // intentionally empty
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 { adapter, outputSchema, middleware, context, debug, ...textOptions } =
2916
- options
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
- for await (const chunk of engine.run()) {
2952
- yield chunk
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