@tanstack/ai 0.26.1 → 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 -6
- package/dist/esm/activities/chat/index.js +78 -27
- 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/middleware/types.d.ts +1 -4
- 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/activities/summarize/chat-stream-summarize.js +62 -3
- package/dist/esm/activities/summarize/chat-stream-summarize.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/logger/internal-logger.d.ts +8 -0
- package/dist/esm/logger/internal-logger.js +15 -0
- package/dist/esm/logger/internal-logger.js.map +1 -1
- package/dist/esm/middlewares/otel.js +30 -6
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/types.d.ts +6 -35
- package/dist/esm/utilities/sampling-keys.d.ts +20 -0
- package/dist/esm/utilities/sampling-keys.js +20 -0
- package/dist/esm/utilities/sampling-keys.js.map +1 -0
- package/package.json +2 -2
- package/skills/ai-core/adapter-configuration/SKILL.md +67 -6
- package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +6 -3
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +3 -0
- package/skills/ai-core/adapter-configuration/references/ollama-adapter.md +10 -1
- package/skills/ai-core/adapter-configuration/references/openai-adapter.md +4 -0
- package/skills/ai-core/chat-experience/SKILL.md +95 -7
- package/skills/ai-core/middleware/SKILL.md +11 -0
- package/skills/ai-core/tool-calling/SKILL.md +287 -0
- package/src/activities/chat/index.ts +97 -35
- package/src/activities/chat/mcp/manager.ts +85 -0
- package/src/activities/chat/mcp/types.ts +66 -0
- package/src/activities/chat/middleware/types.ts +1 -4
- package/src/activities/chat/stream/message-updaters.ts +22 -9
- package/src/activities/chat/tools/tool-calls.ts +2 -0
- package/src/activities/summarize/chat-stream-summarize.ts +162 -3
- package/src/extend-adapter.ts +42 -24
- package/src/index.ts +10 -0
- package/src/logger/internal-logger.ts +18 -0
- package/src/middlewares/otel.ts +48 -6
- package/src/types.ts +6 -35
- package/src/utilities/sampling-keys.ts +28 -0
|
@@ -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,12 +210,12 @@ export interface TextActivityOptions<
|
|
|
208
210
|
| ProviderTool<string, TAdapter['~types']['toolCapabilities'][number]>
|
|
209
211
|
>
|
|
210
212
|
| undefined
|
|
211
|
-
/**
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
217
219
|
/** Additional metadata to attach to the request. */
|
|
218
220
|
metadata?: TextOptions['metadata']
|
|
219
221
|
/** Model-specific provider options (type comes from adapter) */
|
|
@@ -438,6 +440,28 @@ interface TextEngineConfig<
|
|
|
438
440
|
type ToolPhaseResult = 'continue' | 'stop' | 'wait'
|
|
439
441
|
type CyclePhase = 'processText' | 'executeToolCalls'
|
|
440
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
|
+
|
|
441
465
|
class TextEngine<
|
|
442
466
|
TAdapter extends AnyTextAdapter,
|
|
443
467
|
TContext = unknown,
|
|
@@ -492,6 +516,9 @@ class TextEngine<
|
|
|
492
516
|
private readonly deferredPromises: Array<Promise<unknown>> = []
|
|
493
517
|
private abortReason?: string
|
|
494
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
|
|
495
522
|
private terminalHookCalled = false
|
|
496
523
|
|
|
497
524
|
private readonly logger: InternalLogger
|
|
@@ -589,6 +616,10 @@ class TextEngine<
|
|
|
589
616
|
]
|
|
590
617
|
this.middlewareRunner = new MiddlewareRunner(allMiddleware, logger)
|
|
591
618
|
this.middlewareAbortController = new AbortController()
|
|
619
|
+
this.toolAbortSignal = combineAbortSignals(
|
|
620
|
+
this.effectiveSignal,
|
|
621
|
+
this.middlewareAbortController.signal,
|
|
622
|
+
)
|
|
592
623
|
this.middlewareCtx = {
|
|
593
624
|
requestId: this.requestId,
|
|
594
625
|
streamId: this.streamId,
|
|
@@ -844,13 +875,10 @@ class TextEngine<
|
|
|
844
875
|
|
|
845
876
|
private beforeRun(): void {
|
|
846
877
|
this.streamStartTime = Date.now()
|
|
847
|
-
const { tools,
|
|
878
|
+
const { tools, metadata } = this.params
|
|
848
879
|
|
|
849
880
|
// Gather flattened options into an object for context
|
|
850
881
|
const options: Record<string, unknown> = {}
|
|
851
|
-
if (temperature !== undefined) options.temperature = temperature
|
|
852
|
-
if (topP !== undefined) options.topP = topP
|
|
853
|
-
if (maxTokens !== undefined) options.maxTokens = maxTokens
|
|
854
882
|
if (metadata !== undefined) options.metadata = metadata
|
|
855
883
|
|
|
856
884
|
this.eventOptions = Object.keys(options).length > 0 ? options : undefined
|
|
@@ -897,7 +925,7 @@ class TextEngine<
|
|
|
897
925
|
}
|
|
898
926
|
|
|
899
927
|
private async *streamModelResponse(): AsyncGenerator<StreamChunk> {
|
|
900
|
-
const {
|
|
928
|
+
const { metadata, modelOptions } = this.params
|
|
901
929
|
const tools = this.tools
|
|
902
930
|
|
|
903
931
|
// Convert tool schemas to JSON Schema before passing to adapter
|
|
@@ -941,9 +969,6 @@ class TextEngine<
|
|
|
941
969
|
model: this.params.model,
|
|
942
970
|
messages: this.messages,
|
|
943
971
|
tools: toolsWithJsonSchemas,
|
|
944
|
-
temperature,
|
|
945
|
-
topP,
|
|
946
|
-
maxTokens,
|
|
947
972
|
metadata,
|
|
948
973
|
request: this.effectiveRequest,
|
|
949
974
|
modelOptions,
|
|
@@ -1237,6 +1262,7 @@ class TextEngine<
|
|
|
1237
1262
|
},
|
|
1238
1263
|
},
|
|
1239
1264
|
this.middlewareCtx.context,
|
|
1265
|
+
this.toolAbortSignal,
|
|
1240
1266
|
)
|
|
1241
1267
|
|
|
1242
1268
|
// Consume the async generator, yielding custom events and collecting the return value
|
|
@@ -1398,6 +1424,7 @@ class TextEngine<
|
|
|
1398
1424
|
},
|
|
1399
1425
|
},
|
|
1400
1426
|
this.middlewareCtx.context,
|
|
1427
|
+
this.toolAbortSignal,
|
|
1401
1428
|
)
|
|
1402
1429
|
|
|
1403
1430
|
// Consume the async generator, yielding custom events and collecting the return value
|
|
@@ -1869,9 +1896,6 @@ class TextEngine<
|
|
|
1869
1896
|
chatOptions: {
|
|
1870
1897
|
model: this.params.model,
|
|
1871
1898
|
messages: this.messages,
|
|
1872
|
-
temperature: postOnConfig.temperature,
|
|
1873
|
-
topP: postOnConfig.topP,
|
|
1874
|
-
maxTokens: postOnConfig.maxTokens,
|
|
1875
1899
|
metadata: postOnConfig.metadata,
|
|
1876
1900
|
modelOptions: postOnConfig.modelOptions,
|
|
1877
1901
|
systemPrompts: postOnConfig.systemPrompts,
|
|
@@ -2351,9 +2375,6 @@ class TextEngine<
|
|
|
2351
2375
|
messages: this.messages,
|
|
2352
2376
|
systemPrompts: [...this.systemPrompts],
|
|
2353
2377
|
tools: [...this.tools],
|
|
2354
|
-
temperature: this.params.temperature,
|
|
2355
|
-
topP: this.params.topP,
|
|
2356
|
-
maxTokens: this.params.maxTokens,
|
|
2357
2378
|
metadata: this.params.metadata,
|
|
2358
2379
|
modelOptions: this.params.modelOptions,
|
|
2359
2380
|
}
|
|
@@ -2365,9 +2386,6 @@ class TextEngine<
|
|
|
2365
2386
|
this.tools = config.tools
|
|
2366
2387
|
this.params = {
|
|
2367
2388
|
...this.params,
|
|
2368
|
-
temperature: config.temperature,
|
|
2369
|
-
topP: config.topP,
|
|
2370
|
-
maxTokens: config.maxTokens,
|
|
2371
2389
|
metadata: config.metadata,
|
|
2372
2390
|
modelOptions: config.modelOptions,
|
|
2373
2391
|
}
|
|
@@ -2589,10 +2607,16 @@ export function chat<
|
|
|
2589
2607
|
async function* runStreamingText<TContext = unknown>(
|
|
2590
2608
|
options: TextActivityOptions<AnyTextAdapter, undefined, true, TContext>,
|
|
2591
2609
|
): AsyncIterable<StreamChunk> {
|
|
2592
|
-
const { adapter, middleware, context, debug, ...textOptions } = options
|
|
2610
|
+
const { adapter, middleware, context, debug, mcp, ...textOptions } = options
|
|
2593
2611
|
const model = adapter.model
|
|
2594
2612
|
const logger = resolveDebugOption(debug)
|
|
2595
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
|
+
|
|
2596
2620
|
const engine = new TextEngine(
|
|
2597
2621
|
{
|
|
2598
2622
|
adapter,
|
|
@@ -2607,8 +2631,12 @@ async function* runStreamingText<TContext = unknown>(
|
|
|
2607
2631
|
logger,
|
|
2608
2632
|
)
|
|
2609
2633
|
|
|
2610
|
-
|
|
2611
|
-
|
|
2634
|
+
try {
|
|
2635
|
+
for await (const chunk of engine.run()) {
|
|
2636
|
+
yield chunk
|
|
2637
|
+
}
|
|
2638
|
+
} finally {
|
|
2639
|
+
await mcpManager.dispose()
|
|
2612
2640
|
}
|
|
2613
2641
|
}
|
|
2614
2642
|
|
|
@@ -2645,8 +2673,15 @@ async function runAgenticStructuredOutput<
|
|
|
2645
2673
|
>(
|
|
2646
2674
|
options: TextActivityOptions<AnyTextAdapter, TSchema, boolean, TContext>,
|
|
2647
2675
|
): Promise<InferSchemaType<TSchema>> {
|
|
2648
|
-
const {
|
|
2649
|
-
|
|
2676
|
+
const {
|
|
2677
|
+
adapter,
|
|
2678
|
+
outputSchema,
|
|
2679
|
+
middleware,
|
|
2680
|
+
context,
|
|
2681
|
+
debug,
|
|
2682
|
+
mcp,
|
|
2683
|
+
...textOptions
|
|
2684
|
+
} = options
|
|
2650
2685
|
const model = adapter.model
|
|
2651
2686
|
const logger = resolveDebugOption(debug)
|
|
2652
2687
|
|
|
@@ -2680,6 +2715,12 @@ async function runAgenticStructuredOutput<
|
|
|
2680
2715
|
const nativeCombined =
|
|
2681
2716
|
adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true
|
|
2682
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
|
+
|
|
2683
2724
|
const engine = new TextEngine(
|
|
2684
2725
|
{
|
|
2685
2726
|
adapter,
|
|
@@ -2700,9 +2741,13 @@ async function runAgenticStructuredOutput<
|
|
|
2700
2741
|
logger,
|
|
2701
2742
|
)
|
|
2702
2743
|
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
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()
|
|
2706
2751
|
}
|
|
2707
2752
|
|
|
2708
2753
|
const finalizationError = engine.getFinalizationError()
|
|
@@ -2933,8 +2978,15 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
2933
2978
|
options: TextActivityOptions<AnyTextAdapter, TSchema, true, TContext>,
|
|
2934
2979
|
jsonSchema: NonNullable<ReturnType<typeof convertSchemaToJsonSchema>>,
|
|
2935
2980
|
): StructuredOutputStreamInternal<InferSchemaType<TSchema>> {
|
|
2936
|
-
const {
|
|
2937
|
-
|
|
2981
|
+
const {
|
|
2982
|
+
adapter,
|
|
2983
|
+
outputSchema,
|
|
2984
|
+
middleware,
|
|
2985
|
+
context,
|
|
2986
|
+
debug,
|
|
2987
|
+
mcp,
|
|
2988
|
+
...textOptions
|
|
2989
|
+
} = options
|
|
2938
2990
|
const model = adapter.model
|
|
2939
2991
|
const logger = resolveDebugOption(debug)
|
|
2940
2992
|
|
|
@@ -2948,6 +3000,12 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
2948
3000
|
const nativeCombined =
|
|
2949
3001
|
adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true
|
|
2950
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
|
+
|
|
2951
3009
|
// Inputs may be UIMessages (from useChat) or ModelMessages (from server-side
|
|
2952
3010
|
// callers). TextEngine handles the conversion uniformly.
|
|
2953
3011
|
const engine = new TextEngine(
|
|
@@ -2969,8 +3027,12 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
2969
3027
|
logger,
|
|
2970
3028
|
)
|
|
2971
3029
|
|
|
2972
|
-
|
|
2973
|
-
|
|
3030
|
+
try {
|
|
3031
|
+
for await (const chunk of engine.run()) {
|
|
3032
|
+
yield chunk
|
|
3033
|
+
}
|
|
3034
|
+
} finally {
|
|
3035
|
+
await mcpManager.dispose()
|
|
2974
3036
|
}
|
|
2975
3037
|
|
|
2976
3038
|
// Schema validation for the streaming variant remains the consumer's
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ServerTool } from '../tools/tool-definition'
|
|
2
|
+
import type { ChatMCPOptions, MCPToolSource } from './types'
|
|
3
|
+
|
|
4
|
+
export class MCPDuplicateToolNameError extends Error {
|
|
5
|
+
constructor(public readonly toolName: string) {
|
|
6
|
+
super(
|
|
7
|
+
`Duplicate MCP tool name "${toolName}" in chat({ mcp.clients }). ` +
|
|
8
|
+
`Set a unique \`prefix\` on one of the MCP clients (or use a pool, ` +
|
|
9
|
+
`which auto-prefixes) to disambiguate.`,
|
|
10
|
+
)
|
|
11
|
+
this.name = 'MCPDuplicateToolNameError'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Encapsulates MCP tool discovery + connection lifecycle for chat().
|
|
17
|
+
* Built from chat()'s `mcp` option; runners only call `discover()` then
|
|
18
|
+
* `dispose()`. A manager built from `undefined` is an inert no-op
|
|
19
|
+
* (`discover()` → `[]`, `dispose()` → no-op), so runners need no branching.
|
|
20
|
+
*/
|
|
21
|
+
export class MCPManager {
|
|
22
|
+
static from(options: ChatMCPOptions | undefined): MCPManager {
|
|
23
|
+
return new MCPManager(options)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
readonly #sources: ReadonlyArray<MCPToolSource>
|
|
27
|
+
readonly #shouldClose: boolean
|
|
28
|
+
readonly #lazyTools: boolean
|
|
29
|
+
readonly #onDiscoveryError?: (
|
|
30
|
+
error: unknown,
|
|
31
|
+
source: MCPToolSource,
|
|
32
|
+
) => void | Promise<void>
|
|
33
|
+
|
|
34
|
+
private constructor(options: ChatMCPOptions | undefined) {
|
|
35
|
+
this.#sources = options?.clients ?? []
|
|
36
|
+
// default 'close'; only 'keep-alive' disables closing
|
|
37
|
+
this.#shouldClose = options ? options.connection !== 'keep-alive' : false
|
|
38
|
+
this.#lazyTools = options?.lazyTools ?? false
|
|
39
|
+
this.#onDiscoveryError = options?.onDiscoveryError
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Discover + merge tools from all sources. Throws on a fatal discovery error
|
|
44
|
+
* (no `onDiscoveryError`, or it re-threw) or a duplicate tool name; in that
|
|
45
|
+
* case it first closes any connected sources when the policy is 'close'.
|
|
46
|
+
*/
|
|
47
|
+
async discover(): Promise<Array<ServerTool>> {
|
|
48
|
+
if (this.#sources.length === 0) return []
|
|
49
|
+
try {
|
|
50
|
+
const settled = await Promise.allSettled(
|
|
51
|
+
this.#sources.map((s) => s.tools({ lazy: this.#lazyTools })),
|
|
52
|
+
)
|
|
53
|
+
const tools: Array<ServerTool> = []
|
|
54
|
+
const zipped = this.#sources.map(
|
|
55
|
+
(source, i) => [source, settled[i]] as const,
|
|
56
|
+
)
|
|
57
|
+
for (const [source, result] of zipped) {
|
|
58
|
+
if (result === undefined) continue
|
|
59
|
+
if (result.status === 'fulfilled') {
|
|
60
|
+
tools.push(...result.value)
|
|
61
|
+
} else if (this.#onDiscoveryError) {
|
|
62
|
+
// throw/reject inside handler ⇒ propagate (fail-fast); return ⇒ skip
|
|
63
|
+
await this.#onDiscoveryError(result.reason, source)
|
|
64
|
+
} else {
|
|
65
|
+
throw result.reason
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const seen = new Set<string>()
|
|
69
|
+
for (const t of tools) {
|
|
70
|
+
if (seen.has(t.name)) throw new MCPDuplicateToolNameError(t.name)
|
|
71
|
+
seen.add(t.name)
|
|
72
|
+
}
|
|
73
|
+
return tools
|
|
74
|
+
} catch (err) {
|
|
75
|
+
await this.dispose() // cleanup-on-failure (no-op if keep-alive)
|
|
76
|
+
throw err
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Close sources iff policy is 'close'. Idempotent; never throws. */
|
|
81
|
+
async dispose(): Promise<void> {
|
|
82
|
+
if (!this.#shouldClose || this.#sources.length === 0) return
|
|
83
|
+
await Promise.allSettled(this.#sources.map((s) => s.close()))
|
|
84
|
+
}
|
|
85
|
+
}
|