@tanstack/ai-client 0.33.2 → 0.35.1
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/chat-client.d.ts +24 -1
- package/dist/esm/chat-client.js +131 -5
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +2 -2
- package/dist/esm/types.d.ts +84 -7
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/ui/selectors.js +2 -1
- package/dist/esm/ui/selectors.js.map +1 -1
- package/dist/esm/ui/types.d.ts +53 -10
- package/dist/esm/ui.d.ts +1 -1
- package/dist/esm/web-mcp-tools.d.ts +60 -0
- package/dist/esm/web-mcp-tools.js +92 -1
- package/dist/esm/web-mcp-tools.js.map +1 -1
- package/package.json +3 -3
- package/src/chat-client.ts +198 -4
- package/src/index.ts +15 -1
- package/src/types.ts +122 -6
- package/src/ui/selectors.ts +1 -0
- package/src/ui/types.ts +119 -11
- package/src/ui.ts +7 -0
- package/src/web-mcp-tools.ts +181 -0
package/src/types.ts
CHANGED
|
@@ -22,6 +22,8 @@ import type {
|
|
|
22
22
|
SchemaInput,
|
|
23
23
|
StreamChunk,
|
|
24
24
|
StructuredOutputPart,
|
|
25
|
+
SubagentHandleData,
|
|
26
|
+
SubagentStatus,
|
|
25
27
|
UIResourcePart,
|
|
26
28
|
VideoPart,
|
|
27
29
|
} from '@tanstack/ai/client'
|
|
@@ -514,6 +516,20 @@ export interface TextPart {
|
|
|
514
516
|
content: string
|
|
515
517
|
}
|
|
516
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Tool-call part for a bare `{ name }` tool, such as a server tool
|
|
521
|
+
* definition a subagent carries. No `approval` field.
|
|
522
|
+
*/
|
|
523
|
+
type ToolCallPartForNamedTool<T extends { name: string }> = {
|
|
524
|
+
type: 'tool-call'
|
|
525
|
+
id: string
|
|
526
|
+
name: T['name']
|
|
527
|
+
arguments: string
|
|
528
|
+
input?: InferToolInput<T>
|
|
529
|
+
state: ToolCallState
|
|
530
|
+
output?: InferToolOutput<T>
|
|
531
|
+
}
|
|
532
|
+
|
|
517
533
|
/**
|
|
518
534
|
* Helper type that creates a tool-call part for a specific tool.
|
|
519
535
|
* This is a conditional type to enable proper distribution over union types,
|
|
@@ -549,7 +565,9 @@ type ToolCallPartForTool<T> = T extends AnyClientTool
|
|
|
549
565
|
: // Tools without `needsApproval: true` never carry an approval field.
|
|
550
566
|
// `& unknown` is a no-op intersection (adds nothing).
|
|
551
567
|
unknown)
|
|
552
|
-
:
|
|
568
|
+
: T extends { name: string }
|
|
569
|
+
? ToolCallPartForNamedTool<T>
|
|
570
|
+
: never
|
|
553
571
|
|
|
554
572
|
/**
|
|
555
573
|
* Fallback tool-call part type when tools are not typed
|
|
@@ -581,14 +599,14 @@ type UntypedToolCallPart = {
|
|
|
581
599
|
* }
|
|
582
600
|
* ```
|
|
583
601
|
*/
|
|
584
|
-
export type ToolCallPart<TTools extends ReadonlyArray<
|
|
602
|
+
export type ToolCallPart<TTools extends ReadonlyArray<{ name: string }> = any> =
|
|
585
603
|
// Check if we have a concrete tools array (not 'any' or 'never')
|
|
586
604
|
[TTools] extends [never]
|
|
587
605
|
? UntypedToolCallPart
|
|
588
606
|
: unknown extends TTools
|
|
589
607
|
? UntypedToolCallPart
|
|
590
608
|
: TTools extends ReadonlyArray<infer Tool>
|
|
591
|
-
? Tool extends
|
|
609
|
+
? Tool extends { name: string }
|
|
592
610
|
? ToolCallPartForTool<Tool>
|
|
593
611
|
: UntypedToolCallPart
|
|
594
612
|
: UntypedToolCallPart
|
|
@@ -610,9 +628,97 @@ export interface ThinkingPart {
|
|
|
610
628
|
content: string
|
|
611
629
|
}
|
|
612
630
|
|
|
631
|
+
export type { SubagentStatus }
|
|
632
|
+
|
|
633
|
+
export interface SubagentHandle extends SubagentHandleData {
|
|
634
|
+
/**
|
|
635
|
+
* Set by ChatClient on every card it holds, streamed, restored, or initial,
|
|
636
|
+
* nested cards included. Calling it marks the card `error` (`Stopped`),
|
|
637
|
+
* ignores that child's later chunks until the same child starts again, and
|
|
638
|
+
* aborts the local request, parent stream included. A durable server run
|
|
639
|
+
* keeps going.
|
|
640
|
+
*/
|
|
641
|
+
stop?: () => void
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export interface SubagentPart {
|
|
645
|
+
type: 'subagent'
|
|
646
|
+
subagent: SubagentHandle
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* The slice of a server `defineAgent` result that the client reads for types.
|
|
651
|
+
* The client does not call `run`. Put tool definitions (from
|
|
652
|
+
* `toolDefinition`) in `tools` to type the child's tool calls and approvals.
|
|
653
|
+
*/
|
|
654
|
+
export type SubagentClientAgent = {
|
|
655
|
+
name: string
|
|
656
|
+
description?: string
|
|
657
|
+
tools?: ReadonlyArray<{ name: string }>
|
|
658
|
+
interrupts?: ReadonlyArray<InterruptDefinition<any, any, any, any>>
|
|
659
|
+
outputSchema?: SchemaInput
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
type AgentToolList<TAgent> = TAgent extends { tools?: infer TTools }
|
|
663
|
+
? [undefined] extends [TTools]
|
|
664
|
+
? [TTools] extends [undefined]
|
|
665
|
+
? any
|
|
666
|
+
: Exclude<TTools, undefined> extends ReadonlyArray<infer TTool>
|
|
667
|
+
? [TTool] extends [never]
|
|
668
|
+
? any
|
|
669
|
+
: Exclude<TTools, undefined>
|
|
670
|
+
: any
|
|
671
|
+
: TTools extends ReadonlyArray<infer TTool>
|
|
672
|
+
? [TTool] extends [never]
|
|
673
|
+
? any
|
|
674
|
+
: TTools
|
|
675
|
+
: any
|
|
676
|
+
: any
|
|
677
|
+
|
|
678
|
+
type AgentOutputData<TAgent> = TAgent extends { outputSchema?: infer TSchema }
|
|
679
|
+
? Exclude<TSchema, undefined> extends SchemaInput
|
|
680
|
+
? [Exclude<TSchema, undefined>] extends [never]
|
|
681
|
+
? unknown
|
|
682
|
+
: InferSchemaType<Exclude<TSchema, undefined>>
|
|
683
|
+
: unknown
|
|
684
|
+
: unknown
|
|
685
|
+
|
|
686
|
+
/** One child handle. `name` is the discriminant. */
|
|
687
|
+
export type SubagentHandleOf<TAgent extends SubagentClientAgent> = Omit<
|
|
688
|
+
SubagentHandle,
|
|
689
|
+
'name' | 'messages'
|
|
690
|
+
> & {
|
|
691
|
+
name: TAgent['name']
|
|
692
|
+
messages: Array<UIMessage<AgentToolList<TAgent>, AgentOutputData<TAgent>>>
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export type SubagentHandles<
|
|
696
|
+
TAgents extends ReadonlyArray<SubagentClientAgent> | undefined,
|
|
697
|
+
> = [TAgents] extends [undefined]
|
|
698
|
+
? SubagentHandle
|
|
699
|
+
: unknown extends TAgents
|
|
700
|
+
? SubagentHandle
|
|
701
|
+
: TAgents extends ReadonlyArray<infer TAgent>
|
|
702
|
+
? TAgent extends SubagentClientAgent
|
|
703
|
+
? SubagentHandleOf<TAgent>
|
|
704
|
+
: SubagentHandle
|
|
705
|
+
: SubagentHandle
|
|
706
|
+
|
|
707
|
+
export type SubagentPartOf<
|
|
708
|
+
TAgents extends ReadonlyArray<SubagentClientAgent> | undefined,
|
|
709
|
+
> = [TAgents] extends [undefined]
|
|
710
|
+
? SubagentPart
|
|
711
|
+
: unknown extends TAgents
|
|
712
|
+
? SubagentPart
|
|
713
|
+
: {
|
|
714
|
+
type: 'subagent'
|
|
715
|
+
subagent: SubagentHandles<TAgents>
|
|
716
|
+
}
|
|
717
|
+
|
|
613
718
|
export type MessagePart<
|
|
614
|
-
TTools extends ReadonlyArray<
|
|
719
|
+
TTools extends ReadonlyArray<{ name: string }> = any,
|
|
615
720
|
TData = unknown,
|
|
721
|
+
TSubagents extends ReadonlyArray<SubagentClientAgent> | undefined = undefined,
|
|
616
722
|
> =
|
|
617
723
|
| TextPart
|
|
618
724
|
| ImagePart
|
|
@@ -624,6 +730,7 @@ export type MessagePart<
|
|
|
624
730
|
| ThinkingPart
|
|
625
731
|
| StructuredOutputPart<TData>
|
|
626
732
|
| UIResourcePart
|
|
733
|
+
| SubagentPartOf<TSubagents>
|
|
627
734
|
|
|
628
735
|
/**
|
|
629
736
|
* UIMessage - Domain-specific message format optimized for building chat UIs
|
|
@@ -638,13 +745,14 @@ export type MessagePart<
|
|
|
638
745
|
* is typed without manual casts.
|
|
639
746
|
*/
|
|
640
747
|
export interface UIMessage<
|
|
641
|
-
TTools extends ReadonlyArray<
|
|
748
|
+
TTools extends ReadonlyArray<{ name: string }> = any,
|
|
642
749
|
TData = unknown,
|
|
750
|
+
TSubagents extends ReadonlyArray<SubagentClientAgent> | undefined = undefined,
|
|
643
751
|
> {
|
|
644
752
|
id: string
|
|
645
753
|
role: 'system' | 'user' | 'assistant'
|
|
646
754
|
name?: string
|
|
647
|
-
parts: Array<MessagePart<TTools, TData>>
|
|
755
|
+
parts: Array<MessagePart<TTools, TData, TSubagents>>
|
|
648
756
|
createdAt?: Date
|
|
649
757
|
/**
|
|
650
758
|
* Optional AG-UI metadata bag. TanStack writes the `tanstack` key.
|
|
@@ -895,6 +1003,14 @@ export interface ChatClientBaseOptions<
|
|
|
895
1003
|
*/
|
|
896
1004
|
byok?: ByokClient
|
|
897
1005
|
|
|
1006
|
+
/**
|
|
1007
|
+
* The agents you pass to `chat({ subagents: { agents } })`, for types only.
|
|
1008
|
+
* The client does not call `run`. `createChatHook` from
|
|
1009
|
+
* `@tanstack/ai-react/ui` requires a `subagentsComponents` entry for every
|
|
1010
|
+
* agent name.
|
|
1011
|
+
*/
|
|
1012
|
+
subagents?: ReadonlyArray<SubagentClientAgent>
|
|
1013
|
+
|
|
898
1014
|
/**
|
|
899
1015
|
* Optional provider id for this chat. If it returns a provider slug,
|
|
900
1016
|
* only that key is prepared and sent. Otherwise the merged `provider`
|
package/src/ui/selectors.ts
CHANGED
|
@@ -25,6 +25,7 @@ const PART_KEY_BY_TYPE: Record<string, ChatUIPartKey> = {
|
|
|
25
25
|
'tool-result': 'toolResult',
|
|
26
26
|
'structured-output': 'structuredOutput',
|
|
27
27
|
'ui-resource': 'uiResource',
|
|
28
|
+
subagent: 'subagent',
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function partTypeToKey(type: string): ChatUIPartKey | string {
|
package/src/ui/types.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
ChatInterrupt,
|
|
11
11
|
MessagePart,
|
|
12
12
|
RegisteredGenericInterrupt,
|
|
13
|
+
SubagentClientAgent,
|
|
13
14
|
StructuredOutputPart,
|
|
14
15
|
ToolApprovalInterrupt,
|
|
15
16
|
ToolCallPart,
|
|
@@ -28,6 +29,7 @@ export type ChatUIPartKey =
|
|
|
28
29
|
| 'toolResult'
|
|
29
30
|
| 'structuredOutput'
|
|
30
31
|
| 'uiResource'
|
|
32
|
+
| 'subagent'
|
|
31
33
|
|
|
32
34
|
export type ChatUIPartTypeByKey = {
|
|
33
35
|
text: 'text'
|
|
@@ -40,6 +42,7 @@ export type ChatUIPartTypeByKey = {
|
|
|
40
42
|
toolResult: 'tool-result'
|
|
41
43
|
structuredOutput: 'structured-output'
|
|
42
44
|
uiResource: 'ui-resource'
|
|
45
|
+
subagent: 'subagent'
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
export type ChatUIPartOf<
|
|
@@ -60,7 +63,7 @@ export type ChatUIToolsOf<TOptions> = TOptions extends {
|
|
|
60
63
|
: any
|
|
61
64
|
: any
|
|
62
65
|
|
|
63
|
-
|
|
66
|
+
type RootInterruptsOf<TOptions> = TOptions extends {
|
|
64
67
|
interrupts: infer TInterrupts
|
|
65
68
|
}
|
|
66
69
|
? TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>
|
|
@@ -68,6 +71,23 @@ export type ChatUIInterruptsOf<TOptions> = TOptions extends {
|
|
|
68
71
|
: readonly []
|
|
69
72
|
: readonly []
|
|
70
73
|
|
|
74
|
+
type AgentInterrupts<TOptions> = ConcreteItems<
|
|
75
|
+
AgentField<ChatUISubagentsOf<TOptions>[number], 'interrupts', readonly []>
|
|
76
|
+
>
|
|
77
|
+
|
|
78
|
+
/** The root `interrupts` plus every child agent's `interrupts`. */
|
|
79
|
+
export type ChatUIInterruptsOf<TOptions> = [AgentInterrupts<TOptions>] extends [
|
|
80
|
+
never,
|
|
81
|
+
]
|
|
82
|
+
? RootInterruptsOf<TOptions>
|
|
83
|
+
: ReadonlyArray<
|
|
84
|
+
| RootInterruptsOf<TOptions>[number]
|
|
85
|
+
| Extract<
|
|
86
|
+
AgentInterrupts<TOptions>,
|
|
87
|
+
InterruptDefinition<any, any, any, any>
|
|
88
|
+
>
|
|
89
|
+
>
|
|
90
|
+
|
|
71
91
|
export type ChatUISchemaOf<TOptions> = TOptions extends {
|
|
72
92
|
outputSchema: infer TSchema
|
|
73
93
|
}
|
|
@@ -114,14 +134,105 @@ export type ChatUIHasNamedInterrupts<TOptions> = [
|
|
|
114
134
|
? false
|
|
115
135
|
: true
|
|
116
136
|
|
|
137
|
+
/** The agents in `options.subagents`, or an empty list. */
|
|
138
|
+
export type ChatUISubagentsOf<TOptions> = TOptions extends {
|
|
139
|
+
subagents?: infer TAgents
|
|
140
|
+
}
|
|
141
|
+
? Exclude<TAgents, undefined> extends ReadonlyArray<SubagentClientAgent>
|
|
142
|
+
? Exclude<TAgents, undefined>
|
|
143
|
+
: readonly []
|
|
144
|
+
: readonly []
|
|
145
|
+
|
|
146
|
+
export type ChatUISubagentName<TOptions> =
|
|
147
|
+
ChatUISubagentsOf<TOptions>[number]['name']
|
|
148
|
+
|
|
149
|
+
/** The agent in `options.subagents` with this name. */
|
|
150
|
+
export type ChatUISubagentOf<TOptions, TName> = Extract<
|
|
151
|
+
ChatUISubagentsOf<TOptions>[number],
|
|
152
|
+
{ name: TName }
|
|
153
|
+
>
|
|
154
|
+
|
|
155
|
+
type AgentField<TAgent, TKey extends string, TFallback> = TAgent extends {
|
|
156
|
+
[K in TKey]?: infer TValue
|
|
157
|
+
}
|
|
158
|
+
? [Exclude<TValue, undefined>] extends [never]
|
|
159
|
+
? TFallback
|
|
160
|
+
: Exclude<TValue, undefined>
|
|
161
|
+
: TFallback
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* One child agent seen as chat options, so the part, tool, and interrupt
|
|
165
|
+
* types read that agent's `tools`, `interrupts`, and `outputSchema`. An
|
|
166
|
+
* unknown name gives untyped options.
|
|
167
|
+
*/
|
|
168
|
+
export type ChatUISubagentOptions<TOptions, TName> = [
|
|
169
|
+
ChatUISubagentOf<TOptions, TName>,
|
|
170
|
+
] extends [never]
|
|
171
|
+
? {}
|
|
172
|
+
: ChatUISubagentOf<TOptions, TName> extends infer TAgent
|
|
173
|
+
? {
|
|
174
|
+
tools: AgentField<TAgent, 'tools', readonly []>
|
|
175
|
+
interrupts: AgentField<TAgent, 'interrupts', readonly []>
|
|
176
|
+
outputSchema: AgentField<TAgent, 'outputSchema', undefined>
|
|
177
|
+
}
|
|
178
|
+
: {}
|
|
179
|
+
|
|
180
|
+
type ConcreteItems<TList> = unknown extends TList
|
|
181
|
+
? never
|
|
182
|
+
: TList extends ReadonlyArray<infer TItem>
|
|
183
|
+
? TItem
|
|
184
|
+
: never
|
|
185
|
+
|
|
186
|
+
type AgentTools<TOptions> = ConcreteItems<
|
|
187
|
+
AgentField<ChatUISubagentsOf<TOptions>[number], 'tools', readonly []>
|
|
188
|
+
>
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The tools whose approvals can reach this chat: the root `tools` plus every
|
|
192
|
+
* child agent's `tools`.
|
|
193
|
+
*/
|
|
194
|
+
export type ChatUIApprovalToolsOf<TOptions> = [
|
|
195
|
+
ConcreteItems<ChatUIToolsOf<TOptions>> | AgentTools<TOptions>,
|
|
196
|
+
] extends [never]
|
|
197
|
+
? ChatUIToolsOf<TOptions>
|
|
198
|
+
: ReadonlyArray<
|
|
199
|
+
Extract<
|
|
200
|
+
ConcreteItems<ChatUIToolsOf<TOptions>> | AgentTools<TOptions>,
|
|
201
|
+
AnyClientTool
|
|
202
|
+
>
|
|
203
|
+
>
|
|
204
|
+
|
|
205
|
+
export type ChatUIApprovalToolName<TOptions> =
|
|
206
|
+
ChatUIApprovalToolsOf<TOptions>[number] extends infer TTool
|
|
207
|
+
? TTool extends AnyClientTool
|
|
208
|
+
? TTool['name']
|
|
209
|
+
: string
|
|
210
|
+
: string
|
|
211
|
+
|
|
212
|
+
export type ChatUIHasNamedSubagents<TOptions> = [
|
|
213
|
+
ChatUISubagentName<TOptions>,
|
|
214
|
+
] extends [never]
|
|
215
|
+
? false
|
|
216
|
+
: [string] extends [ChatUISubagentName<TOptions>]
|
|
217
|
+
? false
|
|
218
|
+
: true
|
|
219
|
+
|
|
117
220
|
export type ChatUINamedInterruptId<TOptions> = Exclude<
|
|
118
221
|
ChatUIRegisteredInterruptId<TOptions>,
|
|
119
222
|
'fallback'
|
|
120
223
|
>
|
|
121
224
|
|
|
225
|
+
type ChatUIHasNamedApprovalTools<TOptions> = [
|
|
226
|
+
ChatUIApprovalToolName<TOptions>,
|
|
227
|
+
] extends [never]
|
|
228
|
+
? false
|
|
229
|
+
: [string] extends [ChatUIApprovalToolName<TOptions>]
|
|
230
|
+
? false
|
|
231
|
+
: true
|
|
232
|
+
|
|
122
233
|
export type ChatUIInterruptName<TOptions> =
|
|
123
|
-
| (
|
|
124
|
-
?
|
|
234
|
+
| (ChatUIHasNamedApprovalTools<TOptions> extends true
|
|
235
|
+
? ChatUIApprovalToolName<TOptions>
|
|
125
236
|
: never)
|
|
126
237
|
| (ChatUIHasNamedInterrupts<TOptions> extends true
|
|
127
238
|
? ChatUINamedInterruptId<TOptions>
|
|
@@ -136,9 +247,9 @@ export type ChatUIInterrupt = ChatInterrupt | ToolApprovalInterrupt
|
|
|
136
247
|
|
|
137
248
|
export type ChatUIToolApproval<
|
|
138
249
|
TOptions,
|
|
139
|
-
TName extends
|
|
250
|
+
TName extends string = ChatUIApprovalToolName<TOptions>,
|
|
140
251
|
> = Extract<
|
|
141
|
-
ChatInterrupt<
|
|
252
|
+
ChatInterrupt<ChatUIApprovalToolsOf<TOptions>, ChatUIInterruptsOf<TOptions>>,
|
|
142
253
|
{ kind: 'tool-approval'; toolName: TName }
|
|
143
254
|
>
|
|
144
255
|
|
|
@@ -149,10 +260,7 @@ export type ChatUIToolPart<
|
|
|
149
260
|
key: 'toolCall'
|
|
150
261
|
part: Extract<ToolCallPart<ChatUIToolsOf<TOptions>>, { name: TName }>
|
|
151
262
|
result?: ToolResultPart
|
|
152
|
-
interrupt?:
|
|
153
|
-
ChatInterrupt<ChatUIToolsOf<TOptions>, ChatUIInterruptsOf<TOptions>>,
|
|
154
|
-
{ kind: 'tool-approval'; toolName: TName }
|
|
155
|
-
>
|
|
263
|
+
interrupt?: ChatUIToolApproval<TOptions, TName>
|
|
156
264
|
input?: InferToolInput<ToolByName<TOptions, TName>>
|
|
157
265
|
output?: InferToolOutput<ToolByName<TOptions, TName>>
|
|
158
266
|
}
|
|
@@ -172,8 +280,8 @@ export type ChatUIInterruptOf<
|
|
|
172
280
|
> = [TName] extends [never]
|
|
173
281
|
? ChatUIInterrupt
|
|
174
282
|
: TName extends (
|
|
175
|
-
|
|
176
|
-
?
|
|
283
|
+
ChatUIHasNamedApprovalTools<TOptions> extends true
|
|
284
|
+
? ChatUIApprovalToolName<TOptions>
|
|
177
285
|
: never
|
|
178
286
|
)
|
|
179
287
|
? ChatUIToolApproval<TOptions, TName>
|
package/src/ui.ts
CHANGED
|
@@ -6,8 +6,11 @@ export {
|
|
|
6
6
|
selectMessageUI,
|
|
7
7
|
} from './ui/selectors'
|
|
8
8
|
export type {
|
|
9
|
+
ChatUIApprovalToolName,
|
|
10
|
+
ChatUIApprovalToolsOf,
|
|
9
11
|
ChatUIData,
|
|
10
12
|
ChatUIHasNamedInterrupts,
|
|
13
|
+
ChatUIHasNamedSubagents,
|
|
11
14
|
ChatUIHasNamedTools,
|
|
12
15
|
ChatUIInterrupt,
|
|
13
16
|
ChatUIInterruptName,
|
|
@@ -28,6 +31,10 @@ export type {
|
|
|
28
31
|
ChatUISelectedResultPart,
|
|
29
32
|
ChatUISelectedToolPart,
|
|
30
33
|
ChatUISelection,
|
|
34
|
+
ChatUISubagentName,
|
|
35
|
+
ChatUISubagentOf,
|
|
36
|
+
ChatUISubagentOptions,
|
|
37
|
+
ChatUISubagentsOf,
|
|
31
38
|
ChatUIStructuredPart,
|
|
32
39
|
ChatUIToolApproval,
|
|
33
40
|
ChatUIToolName,
|
package/src/web-mcp-tools.ts
CHANGED
|
@@ -209,3 +209,184 @@ export async function registerWebMCPTools<
|
|
|
209
209
|
throw error
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
+
|
|
213
|
+
/** A tool that a page registered with WebMCP, as `getTools()` returns it. */
|
|
214
|
+
export interface WebMCPPageTool {
|
|
215
|
+
name: string
|
|
216
|
+
title?: string
|
|
217
|
+
description: string
|
|
218
|
+
/** A JSON Schema object for the tool input. */
|
|
219
|
+
inputSchema?: object
|
|
220
|
+
/** The origin of the document that registered the tool. */
|
|
221
|
+
origin: string
|
|
222
|
+
annotations?: WebMCPToolAnnotations
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
interface WebMCPToolReader {
|
|
226
|
+
getTools: () => Promise<Array<WebMCPPageTool>>
|
|
227
|
+
executeTool: (
|
|
228
|
+
tool: WebMCPPageTool,
|
|
229
|
+
input: unknown,
|
|
230
|
+
options: { signal?: AbortSignal },
|
|
231
|
+
) => Promise<string>
|
|
232
|
+
addEventListener: EventTarget['addEventListener']
|
|
233
|
+
removeEventListener: EventTarget['removeEventListener']
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Options for {@link getWebMCPTools}. */
|
|
237
|
+
export interface GetWebMCPToolsOptions {
|
|
238
|
+
/** Return `false` to skip a tool. */
|
|
239
|
+
filter?: (tool: WebMCPPageTool) => boolean
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Options for {@link subscribeWebMCPTools}. */
|
|
243
|
+
export interface SubscribeWebMCPToolsOptions extends GetWebMCPToolsOptions {
|
|
244
|
+
/** Stops the subscription when it aborts. */
|
|
245
|
+
signal: AbortSignal
|
|
246
|
+
/** Receives a failure from the WebMCP `getTools()` call. */
|
|
247
|
+
onError?: (error: unknown) => void
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isWebMCPToolReader(value: unknown): value is WebMCPToolReader {
|
|
251
|
+
return (
|
|
252
|
+
value !== null &&
|
|
253
|
+
typeof value === 'object' &&
|
|
254
|
+
'getTools' in value &&
|
|
255
|
+
typeof value.getTools === 'function' &&
|
|
256
|
+
'executeTool' in value &&
|
|
257
|
+
typeof value.executeTool === 'function' &&
|
|
258
|
+
'addEventListener' in value &&
|
|
259
|
+
typeof value.addEventListener === 'function' &&
|
|
260
|
+
'removeEventListener' in value &&
|
|
261
|
+
typeof value.removeEventListener === 'function'
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function getWebMCPToolReader() {
|
|
266
|
+
if (
|
|
267
|
+
typeof document === 'undefined' ||
|
|
268
|
+
(typeof isSecureContext !== 'undefined' && !isSecureContext) ||
|
|
269
|
+
!('modelContext' in document) ||
|
|
270
|
+
!isWebMCPToolReader(document.modelContext)
|
|
271
|
+
) {
|
|
272
|
+
return undefined
|
|
273
|
+
}
|
|
274
|
+
return document.modelContext
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function parseToolResult(result: string): unknown {
|
|
278
|
+
try {
|
|
279
|
+
return JSON.parse(result)
|
|
280
|
+
} catch {
|
|
281
|
+
return result
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function readWebMCPTools(
|
|
286
|
+
reader: WebMCPToolReader,
|
|
287
|
+
options: GetWebMCPToolsOptions | undefined,
|
|
288
|
+
): Promise<Array<AnyClientTool>> {
|
|
289
|
+
const pageTools = await reader.getTools()
|
|
290
|
+
const names = new Set<string>()
|
|
291
|
+
return pageTools
|
|
292
|
+
.filter((tool) => options?.filter?.(tool) ?? true)
|
|
293
|
+
.map((tool) => {
|
|
294
|
+
if (names.has(tool.name)) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`Duplicate WebMCP tool name "${tool.name}". Use a filter or register tools with unique names.`,
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
names.add(tool.name)
|
|
300
|
+
return {
|
|
301
|
+
__toolSide: 'client' as const,
|
|
302
|
+
name: tool.name,
|
|
303
|
+
description: tool.description,
|
|
304
|
+
inputSchema: tool.inputSchema ?? { type: 'object' },
|
|
305
|
+
async execute(input: unknown, context?: { abortSignal?: AbortSignal }) {
|
|
306
|
+
const result = await reader.executeTool(
|
|
307
|
+
tool,
|
|
308
|
+
input,
|
|
309
|
+
context?.abortSignal ? { signal: context.abortSignal } : {},
|
|
310
|
+
)
|
|
311
|
+
return parseToolResult(result)
|
|
312
|
+
},
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Reads the WebMCP tools on the page and returns them as client tools.
|
|
319
|
+
*
|
|
320
|
+
* Pass the result to a chat as `tools`. Each tool runs through the WebMCP
|
|
321
|
+
* `executeTool()` call. Unsupported browsers and server environments return
|
|
322
|
+
* an empty array.
|
|
323
|
+
*
|
|
324
|
+
* @param options - A filter that skips tools.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```ts
|
|
328
|
+
* const tools = await getWebMCPTools({
|
|
329
|
+
* filter: (tool) => tool.origin === location.origin,
|
|
330
|
+
* })
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
export async function getWebMCPTools(
|
|
334
|
+
options?: GetWebMCPToolsOptions,
|
|
335
|
+
): Promise<Array<AnyClientTool>> {
|
|
336
|
+
const reader = getWebMCPToolReader()
|
|
337
|
+
return reader ? readWebMCPTools(reader, options) : []
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Calls `listener` with the page WebMCP tools now and after each
|
|
342
|
+
* `toolchange` event, until `options.signal` aborts.
|
|
343
|
+
*
|
|
344
|
+
* Unsupported browsers and server environments call `listener` once with an
|
|
345
|
+
* empty array. When a read fails, `options.onError` gets the error and the
|
|
346
|
+
* listener keeps the last list.
|
|
347
|
+
*
|
|
348
|
+
* @param listener - Receives the current client tools.
|
|
349
|
+
* @param options - The subscription signal, a filter, and an error callback.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```ts
|
|
353
|
+
* const controller = new AbortController()
|
|
354
|
+
* subscribeWebMCPTools((tools) => client.updateOptions({ tools }), {
|
|
355
|
+
* signal: controller.signal,
|
|
356
|
+
* })
|
|
357
|
+
* ```
|
|
358
|
+
*/
|
|
359
|
+
export function subscribeWebMCPTools(
|
|
360
|
+
listener: (tools: Array<AnyClientTool>) => void,
|
|
361
|
+
options: SubscribeWebMCPToolsOptions,
|
|
362
|
+
): void {
|
|
363
|
+
if (options.signal.aborted) return
|
|
364
|
+
const reader = getWebMCPToolReader()
|
|
365
|
+
if (!reader) {
|
|
366
|
+
listener([])
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let latestRead = 0
|
|
371
|
+
const refresh = () => {
|
|
372
|
+
const read = ++latestRead
|
|
373
|
+
const isCurrent = () => read === latestRead && !options.signal.aborted
|
|
374
|
+
readWebMCPTools(reader, options).then(
|
|
375
|
+
(tools) => {
|
|
376
|
+
if (isCurrent()) listener(tools)
|
|
377
|
+
},
|
|
378
|
+
(error: unknown) => {
|
|
379
|
+
if (isCurrent()) options.onError?.(error)
|
|
380
|
+
},
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Remove the listener by hand: Zone.js breaks the `signal` listener option.
|
|
385
|
+
reader.addEventListener('toolchange', refresh)
|
|
386
|
+
options.signal.addEventListener(
|
|
387
|
+
'abort',
|
|
388
|
+
() => reader.removeEventListener('toolchange', refresh),
|
|
389
|
+
{ once: true },
|
|
390
|
+
)
|
|
391
|
+
refresh()
|
|
392
|
+
}
|