@tanstack/ai-client 0.29.2 → 0.31.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.
@@ -0,0 +1,178 @@
1
+ import type {
2
+ MessagePart,
3
+ ToolApprovalInterrupt,
4
+ ToolCallPart,
5
+ ToolResultPart,
6
+ UIMessage,
7
+ } from '../types'
8
+ import type {
9
+ ChatUIInterrupt,
10
+ ChatUIPartKey,
11
+ ChatUISelectInput,
12
+ ChatUISelectedMessage,
13
+ ChatUISelectedPart,
14
+ ChatUISelection,
15
+ } from './types'
16
+
17
+ const PART_KEY_BY_TYPE: Record<string, ChatUIPartKey> = {
18
+ text: 'text',
19
+ image: 'image',
20
+ audio: 'audio',
21
+ video: 'video',
22
+ document: 'document',
23
+ thinking: 'thinking',
24
+ 'tool-call': 'toolCall',
25
+ 'tool-result': 'toolResult',
26
+ 'structured-output': 'structuredOutput',
27
+ 'ui-resource': 'uiResource',
28
+ }
29
+
30
+ export function partTypeToKey(type: string): ChatUIPartKey | string {
31
+ const mapped = PART_KEY_BY_TYPE[type]
32
+ if (mapped) return mapped
33
+ return type.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase())
34
+ }
35
+
36
+ export function collectInlineToolNames(
37
+ toolInterrupts?: Record<string, unknown>,
38
+ mappedToolNames?: ReadonlyArray<string>,
39
+ ): Array<string> {
40
+ const listPlaced = new Set(Object.keys(toolInterrupts ?? {}))
41
+ return (mappedToolNames ?? []).filter((name) => !listPlaced.has(name))
42
+ }
43
+
44
+ export function resolveInterruptComponent(
45
+ interrupt: ChatUIInterrupt,
46
+ interruptsMap:
47
+ | {
48
+ tools?: Record<string, unknown>
49
+ generic?: Record<string, unknown>
50
+ }
51
+ | undefined,
52
+ ): unknown {
53
+ if (!interruptsMap) return undefined
54
+ if (interrupt.kind === 'tool-approval') {
55
+ return interruptsMap.tools?.[interrupt.toolName]
56
+ }
57
+ const generic = interruptsMap.generic
58
+ if (!generic) return undefined
59
+ const definitionId =
60
+ 'definitionId' in interrupt && typeof interrupt.definitionId === 'string'
61
+ ? interrupt.definitionId
62
+ : undefined
63
+ if (definitionId && definitionId !== 'fallback') {
64
+ const registered = generic[definitionId]
65
+ if (registered) return registered
66
+ }
67
+ return generic.fallback
68
+ }
69
+
70
+ function isToolCallPart(part: MessagePart): part is ToolCallPart {
71
+ return part.type === 'tool-call'
72
+ }
73
+
74
+ function isToolResultPart(part: MessagePart): part is ToolResultPart {
75
+ return part.type === 'tool-result'
76
+ }
77
+
78
+ function isToolApproval(
79
+ interrupt: ChatUIInterrupt,
80
+ ): interrupt is ToolApprovalInterrupt {
81
+ return interrupt.kind === 'tool-approval'
82
+ }
83
+
84
+ export function selectChatUI(input: ChatUISelectInput): ChatUISelection {
85
+ const messages = input.messages
86
+ const interrupts = input.interrupts ?? []
87
+ const inlineToolNames = new Set(input.inlineToolNames ?? [])
88
+
89
+ const resultsByCallId = new Map<string, ToolResultPart>()
90
+ const callIds = new Set<string>()
91
+ for (const message of messages) {
92
+ for (const part of message.parts) {
93
+ if (isToolCallPart(part)) callIds.add(part.id)
94
+ if (isToolResultPart(part)) resultsByCallId.set(part.toolCallId, part)
95
+ }
96
+ }
97
+
98
+ const approvalByCallId = new Map<string, ToolApprovalInterrupt>()
99
+ for (const interrupt of interrupts) {
100
+ if (isToolApproval(interrupt)) {
101
+ approvalByCallId.set(interrupt.toolCallId, interrupt)
102
+ }
103
+ }
104
+
105
+ const selectedMessages: Array<ChatUISelectedMessage> = messages.map(
106
+ (message) => ({
107
+ message,
108
+ parts: message.parts.map((part) =>
109
+ toSelectedPart(part, resultsByCallId, approvalByCallId, callIds),
110
+ ),
111
+ }),
112
+ )
113
+
114
+ return {
115
+ messages: selectedMessages,
116
+ interrupts: interrupts.filter((interrupt) => {
117
+ if (!isToolApproval(interrupt)) return true
118
+ if (!inlineToolNames.has(interrupt.toolName)) return true
119
+ return !callIds.has(interrupt.toolCallId)
120
+ }),
121
+ }
122
+ }
123
+
124
+ function toSelectedPart(
125
+ part: MessagePart,
126
+ resultsByCallId: Map<string, ToolResultPart>,
127
+ approvalByCallId: Map<string, ToolApprovalInterrupt>,
128
+ callIds: Set<string>,
129
+ ): ChatUISelectedPart {
130
+ if (isToolCallPart(part)) {
131
+ const result = resultsByCallId.get(part.id)
132
+ const interrupt = approvalByCallId.get(part.id)
133
+ return {
134
+ key: 'toolCall',
135
+ part,
136
+ ...(result ? { result } : {}),
137
+ ...(interrupt ? { interrupt } : {}),
138
+ input: part.input,
139
+ output: part.output,
140
+ }
141
+ }
142
+
143
+ if (isToolResultPart(part)) {
144
+ return {
145
+ key: 'toolResult',
146
+ part,
147
+ matched: callIds.has(part.toolCallId),
148
+ }
149
+ }
150
+
151
+ return {
152
+ key: partTypeToKey(part.type) as Exclude<
153
+ ChatUIPartKey,
154
+ 'toolCall' | 'toolResult'
155
+ >,
156
+ part,
157
+ }
158
+ }
159
+
160
+ export function automaticPartsForMessage(
161
+ selected: ChatUISelectedMessage,
162
+ ): Array<ChatUISelectedPart> {
163
+ return selected.parts.filter(
164
+ (part) => !(part.key === 'toolResult' && part.matched),
165
+ )
166
+ }
167
+
168
+ export function selectMessageUI(
169
+ message: UIMessage,
170
+ input: Omit<ChatUISelectInput, 'messages'>,
171
+ ): ChatUISelectedMessage {
172
+ return (
173
+ selectChatUI({ ...input, messages: [message] }).messages[0] ?? {
174
+ message,
175
+ parts: [],
176
+ }
177
+ )
178
+ }
@@ -0,0 +1,243 @@
1
+ import type {
2
+ AnyClientTool,
3
+ InferSchemaType,
4
+ InferToolInput,
5
+ InferToolOutput,
6
+ InterruptDefinition,
7
+ SchemaInput,
8
+ } from '@tanstack/ai/client'
9
+ import type {
10
+ ChatInterrupt,
11
+ MessagePart,
12
+ RegisteredGenericInterrupt,
13
+ StructuredOutputPart,
14
+ ToolApprovalInterrupt,
15
+ ToolCallPart,
16
+ ToolResultPart,
17
+ UIMessage,
18
+ } from '../types'
19
+
20
+ export type ChatUIPartKey =
21
+ | 'text'
22
+ | 'image'
23
+ | 'audio'
24
+ | 'video'
25
+ | 'document'
26
+ | 'thinking'
27
+ | 'toolCall'
28
+ | 'toolResult'
29
+ | 'structuredOutput'
30
+ | 'uiResource'
31
+
32
+ export type ChatUIPartTypeByKey = {
33
+ text: 'text'
34
+ image: 'image'
35
+ audio: 'audio'
36
+ video: 'video'
37
+ document: 'document'
38
+ thinking: 'thinking'
39
+ toolCall: 'tool-call'
40
+ toolResult: 'tool-result'
41
+ structuredOutput: 'structured-output'
42
+ uiResource: 'ui-resource'
43
+ }
44
+
45
+ export type ChatUIPartOf<
46
+ TOptions,
47
+ TKey extends ChatUIPartKey = ChatUIPartKey,
48
+ > = TKey extends ChatUIPartKey
49
+ ? Extract<
50
+ MessagePart<ChatUIToolsOf<TOptions>, ChatUIData<TOptions>>,
51
+ { type: ChatUIPartTypeByKey[TKey] }
52
+ >
53
+ : never
54
+
55
+ export type ChatUIToolsOf<TOptions> = TOptions extends {
56
+ tools: infer TTools
57
+ }
58
+ ? TTools extends ReadonlyArray<AnyClientTool>
59
+ ? TTools
60
+ : any
61
+ : any
62
+
63
+ export type ChatUIInterruptsOf<TOptions> = TOptions extends {
64
+ interrupts: infer TInterrupts
65
+ }
66
+ ? TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>
67
+ ? TInterrupts
68
+ : readonly []
69
+ : readonly []
70
+
71
+ export type ChatUISchemaOf<TOptions> = TOptions extends {
72
+ outputSchema: infer TSchema
73
+ }
74
+ ? TSchema extends SchemaInput
75
+ ? TSchema
76
+ : undefined
77
+ : undefined
78
+
79
+ export type ChatUIData<TOptions> = TOptions extends {
80
+ outputSchema: infer TSchema
81
+ }
82
+ ? TSchema extends SchemaInput
83
+ ? InferSchemaType<TSchema>
84
+ : unknown
85
+ : unknown
86
+
87
+ export type ChatUIToolName<TOptions> =
88
+ ChatUIToolsOf<TOptions>[number] extends infer TTool
89
+ ? TTool extends AnyClientTool
90
+ ? TTool['name']
91
+ : string
92
+ : string
93
+
94
+ export type ChatUIRegisteredInterruptId<TOptions> =
95
+ ChatUIInterruptsOf<TOptions>[number] extends infer TDefinition
96
+ ? TDefinition extends InterruptDefinition<infer TId, any, any, any>
97
+ ? TId
98
+ : string
99
+ : string
100
+
101
+ export type ChatUIHasNamedTools<TOptions> = [ChatUIToolName<TOptions>] extends [
102
+ never,
103
+ ]
104
+ ? false
105
+ : [string] extends [ChatUIToolName<TOptions>]
106
+ ? false
107
+ : true
108
+
109
+ export type ChatUIHasNamedInterrupts<TOptions> = [
110
+ ChatUIRegisteredInterruptId<TOptions>,
111
+ ] extends [never]
112
+ ? false
113
+ : [string] extends [ChatUIRegisteredInterruptId<TOptions>]
114
+ ? false
115
+ : true
116
+
117
+ export type ChatUINamedInterruptId<TOptions> = Exclude<
118
+ ChatUIRegisteredInterruptId<TOptions>,
119
+ 'fallback'
120
+ >
121
+
122
+ export type ChatUIInterruptName<TOptions> =
123
+ | (ChatUIHasNamedTools<TOptions> extends true
124
+ ? ChatUIToolName<TOptions>
125
+ : never)
126
+ | (ChatUIHasNamedInterrupts<TOptions> extends true
127
+ ? ChatUINamedInterruptId<TOptions>
128
+ : never)
129
+
130
+ type ToolByName<TOptions, TName> = Extract<
131
+ ChatUIToolsOf<TOptions> extends ReadonlyArray<infer TTool> ? TTool : never,
132
+ { name: TName }
133
+ >
134
+
135
+ export type ChatUIInterrupt = ChatInterrupt | ToolApprovalInterrupt
136
+
137
+ export type ChatUIToolApproval<
138
+ TOptions,
139
+ TName extends ChatUIToolName<TOptions> = ChatUIToolName<TOptions>,
140
+ > = Extract<
141
+ ChatInterrupt<ChatUIToolsOf<TOptions>, ChatUIInterruptsOf<TOptions>>,
142
+ { kind: 'tool-approval'; toolName: TName }
143
+ >
144
+
145
+ export type ChatUIToolPart<
146
+ TOptions,
147
+ TName extends ChatUIToolName<TOptions> = ChatUIToolName<TOptions>,
148
+ > = {
149
+ key: 'toolCall'
150
+ part: Extract<ToolCallPart<ChatUIToolsOf<TOptions>>, { name: TName }>
151
+ result?: ToolResultPart
152
+ interrupt?: Extract<
153
+ ChatInterrupt<ChatUIToolsOf<TOptions>, ChatUIInterruptsOf<TOptions>>,
154
+ { kind: 'tool-approval'; toolName: TName }
155
+ >
156
+ input?: InferToolInput<ToolByName<TOptions, TName>>
157
+ output?: InferToolOutput<ToolByName<TOptions, TName>>
158
+ }
159
+
160
+ export type RegisteredUIInterrupt<
161
+ TOptions,
162
+ TId extends ChatUIRegisteredInterruptId<TOptions> =
163
+ ChatUIRegisteredInterruptId<TOptions>,
164
+ > = Extract<
165
+ RegisteredGenericInterrupt<ChatUIInterruptsOf<TOptions>>,
166
+ { definitionId: TId }
167
+ >
168
+
169
+ export type ChatUIInterruptOf<
170
+ TOptions,
171
+ TName extends ChatUIInterruptName<TOptions> = never,
172
+ > = [TName] extends [never]
173
+ ? ChatUIInterrupt
174
+ : TName extends (
175
+ ChatUIHasNamedTools<TOptions> extends true
176
+ ? ChatUIToolName<TOptions>
177
+ : never
178
+ )
179
+ ? ChatUIToolApproval<TOptions, TName>
180
+ : TName extends ChatUINamedInterruptId<TOptions>
181
+ ? RegisteredUIInterrupt<TOptions, TName>
182
+ : ChatUIInterrupt
183
+
184
+ export type ChatUISelectedToolPart = {
185
+ key: 'toolCall'
186
+ part: ToolCallPart
187
+ result?: ToolResultPart
188
+ interrupt?: ChatUIInterrupt
189
+ input?: unknown
190
+ output?: unknown
191
+ }
192
+
193
+ export type ChatUISelectedResultPart = {
194
+ key: 'toolResult'
195
+ part: ToolResultPart
196
+ matched: boolean
197
+ }
198
+
199
+ export type ChatUISelectedGenericPart = {
200
+ key: Exclude<ChatUIPartKey, 'toolCall' | 'toolResult'>
201
+ part: MessagePart
202
+ }
203
+
204
+ export type ChatUISelectedPart =
205
+ | ChatUISelectedToolPart
206
+ | ChatUISelectedResultPart
207
+ | ChatUISelectedGenericPart
208
+
209
+ export type ChatUISelectedPartOf<
210
+ TOptions,
211
+ TKey extends ChatUIPartKey = ChatUIPartKey,
212
+ > = TKey extends 'toolCall'
213
+ ? ChatUIToolPart<TOptions>
214
+ : TKey extends 'toolResult'
215
+ ? ChatUISelectedResultPart
216
+ : {
217
+ key: TKey
218
+ part: ChatUIPartOf<TOptions, TKey>
219
+ }
220
+
221
+ export type ChatUISelectedMessage = {
222
+ message: UIMessage
223
+ parts: Array<ChatUISelectedPart>
224
+ }
225
+
226
+ export type ChatUISelection = {
227
+ messages: Array<ChatUISelectedMessage>
228
+ interrupts: Array<ChatUIInterrupt>
229
+ }
230
+
231
+ export type ChatUISelectInput = {
232
+ messages: ReadonlyArray<UIMessage>
233
+ interrupts?: ReadonlyArray<ChatUIInterrupt>
234
+ inlineToolNames?: ReadonlyArray<string>
235
+ }
236
+
237
+ export type ChatUIMessages<TOptions> = Array<
238
+ UIMessage<ChatUIToolsOf<TOptions>, ChatUIData<TOptions>>
239
+ >
240
+
241
+ export type ChatUIStructuredPart<TOptions> = StructuredOutputPart<
242
+ ChatUIData<TOptions>
243
+ >
package/src/ui.ts ADDED
@@ -0,0 +1,37 @@
1
+ export { partTypeToKey, selectChatUI } from './ui/selectors'
2
+ export {
3
+ automaticPartsForMessage,
4
+ collectInlineToolNames,
5
+ resolveInterruptComponent,
6
+ selectMessageUI,
7
+ } from './ui/selectors'
8
+ export type {
9
+ ChatUIData,
10
+ ChatUIHasNamedInterrupts,
11
+ ChatUIHasNamedTools,
12
+ ChatUIInterrupt,
13
+ ChatUIInterruptName,
14
+ ChatUIInterruptOf,
15
+ ChatUIInterruptsOf,
16
+ ChatUINamedInterruptId,
17
+ ChatUIMessages,
18
+ ChatUIPartKey,
19
+ ChatUIPartOf,
20
+ ChatUIPartTypeByKey,
21
+ ChatUIRegisteredInterruptId,
22
+ ChatUISchemaOf,
23
+ ChatUISelectInput,
24
+ ChatUISelectedGenericPart,
25
+ ChatUISelectedMessage,
26
+ ChatUISelectedPart,
27
+ ChatUISelectedPartOf,
28
+ ChatUISelectedResultPart,
29
+ ChatUISelectedToolPart,
30
+ ChatUISelection,
31
+ ChatUIStructuredPart,
32
+ ChatUIToolApproval,
33
+ ChatUIToolName,
34
+ ChatUIToolPart,
35
+ ChatUIToolsOf,
36
+ RegisteredUIInterrupt,
37
+ } from './ui/types'
@@ -0,0 +1,211 @@
1
+ import {
2
+ convertSchemaToJsonSchema,
3
+ validateWithStandardSchema,
4
+ } from '@tanstack/ai/client'
5
+ import type { AnyClientTool } from '@tanstack/ai/client'
6
+ import type {
7
+ ClientContextOptionFromTools,
8
+ InferredClientContext,
9
+ } from './types'
10
+
11
+ interface WebMCPTool {
12
+ name: string
13
+ title?: string
14
+ description: string
15
+ inputSchema?: object
16
+ annotations?: WebMCPToolAnnotations
17
+ execute: (input: object, options: { signal: AbortSignal }) => Promise<unknown>
18
+ }
19
+
20
+ interface WebMCPModelContext {
21
+ registerTool: (
22
+ tool: WebMCPTool,
23
+ options: { signal: AbortSignal },
24
+ ) => Promise<void>
25
+ }
26
+
27
+ /** WebMCP behavior hints for one registered tool. */
28
+ export interface WebMCPToolAnnotations {
29
+ /** Indicates that the tool does not modify state. */
30
+ readOnlyHint?: boolean
31
+ /** Indicates that the tool can return content that the application does not trust. */
32
+ untrustedContentHint?: boolean
33
+ }
34
+
35
+ /** Display and behavior options for one WebMCP tool. */
36
+ export interface WebMCPToolOptions {
37
+ /** A human-readable title for browser user interfaces. */
38
+ title?: string
39
+ /** Optional behavior hints for browser agents. */
40
+ annotations?: WebMCPToolAnnotations
41
+ }
42
+
43
+ /** WebMCP options keyed by the inferred names in a client tool list. */
44
+ export type WebMCPToolOptionsByName<
45
+ TTools extends ReadonlyArray<AnyClientTool>,
46
+ > = Partial<{
47
+ [TName in TTools[number]['name']]: WebMCPToolOptions
48
+ }>
49
+
50
+ /**
51
+ * Options for {@link registerWebMCPTools}.
52
+ *
53
+ * The signal controls the registration lifetime. Context is required when a
54
+ * client tool declares a required runtime context.
55
+ */
56
+ export type RegisterWebMCPToolsOptions<
57
+ TTools extends ReadonlyArray<AnyClientTool>,
58
+ TContext = InferredClientContext<TTools>,
59
+ > = {
60
+ /** Removes all tools from this call when the signal aborts. */
61
+ signal: AbortSignal
62
+ /** Per-tool display and behavior options. */
63
+ toolOptions?: WebMCPToolOptionsByName<TTools>
64
+ } & ClientContextOptionFromTools<TTools, TContext>
65
+
66
+ function isWebMCPModelContext(value: unknown): value is WebMCPModelContext {
67
+ return (
68
+ value !== null &&
69
+ typeof value === 'object' &&
70
+ 'registerTool' in value &&
71
+ typeof value.registerTool === 'function'
72
+ )
73
+ }
74
+
75
+ function getToolOptions<TName extends string>(
76
+ toolOptions: Partial<Record<TName, WebMCPToolOptions>> | undefined,
77
+ name: TName,
78
+ ) {
79
+ return toolOptions?.[name]
80
+ }
81
+
82
+ async function validateSchemaValue(schema: unknown, value: unknown) {
83
+ const result = await validateWithStandardSchema(schema, value)
84
+ if (result.success) {
85
+ return result.data
86
+ }
87
+
88
+ throw new Error(
89
+ `Validation failed: ${result.issues.map((issue) => issue.message).join(', ')}`,
90
+ )
91
+ }
92
+
93
+ /**
94
+ * Registers executable TanStack client tools with the browser WebMCP API.
95
+ *
96
+ * Unsupported browsers and server environments resolve without registration.
97
+ * Abort `options.signal` to remove every tool registered by this call.
98
+ *
99
+ * @param tools - The executable client tools to expose through WebMCP.
100
+ * @param options - The registration signal, runtime context, and per-tool options.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const controller = new AbortController()
105
+ * await registerWebMCPTools(tools, { signal: controller.signal })
106
+ * controller.abort()
107
+ * ```
108
+ */
109
+ export async function registerWebMCPTools<
110
+ const TTools extends ReadonlyArray<AnyClientTool>,
111
+ TContext = InferredClientContext<TTools>,
112
+ >(tools: TTools, options: RegisterWebMCPToolsOptions<TTools, TContext>) {
113
+ if (
114
+ typeof document === 'undefined' ||
115
+ (typeof isSecureContext !== 'undefined' && !isSecureContext) ||
116
+ !('modelContext' in document) ||
117
+ !isWebMCPModelContext(document.modelContext)
118
+ ) {
119
+ return
120
+ }
121
+ if (tools.length === 0) {
122
+ return
123
+ }
124
+
125
+ const names = new Set<string>()
126
+ const webMCPTools = tools.map((tool) => {
127
+ if (!/^[A-Za-z0-9_.-]{1,128}$/.test(tool.name)) {
128
+ throw new Error(
129
+ `WebMCP tool name "${tool.name}" must contain 1 to 128 ASCII letters, numbers, underscores, hyphens, or periods.`,
130
+ )
131
+ }
132
+ if (names.has(tool.name)) {
133
+ throw new Error(`Duplicate WebMCP tool name "${tool.name}".`)
134
+ }
135
+ if (tool.description.trim() === '') {
136
+ throw new Error(`WebMCP tool "${tool.name}" must have a description.`)
137
+ }
138
+ if (typeof tool.execute !== 'function') {
139
+ throw new Error(
140
+ `WebMCP tool "${tool.name}" must have an execute handler.`,
141
+ )
142
+ }
143
+ if (tool.needsApproval === true) {
144
+ throw new Error(
145
+ `WebMCP tool "${tool.name}" cannot use needsApproval: true.`,
146
+ )
147
+ }
148
+
149
+ names.add(tool.name)
150
+ const toolOptions = getToolOptions(options.toolOptions, tool.name)
151
+ const inputSchema = tool.inputSchema
152
+ const outputSchema = tool.outputSchema
153
+ const convertedInputSchema = convertSchemaToJsonSchema(inputSchema)
154
+ const inputSchemaType = convertedInputSchema?.type
155
+ const requiresNonObjectInput =
156
+ (typeof inputSchemaType === 'string' && inputSchemaType !== 'object') ||
157
+ (Array.isArray(inputSchemaType) && !inputSchemaType.includes('object'))
158
+ if (requiresNonObjectInput) {
159
+ throw new Error(
160
+ `WebMCP tool "${tool.name}" input schema must accept an object.`,
161
+ )
162
+ }
163
+ const execute = tool.execute
164
+
165
+ return {
166
+ name: tool.name,
167
+ description: tool.description,
168
+ ...(toolOptions?.title !== undefined ? { title: toolOptions.title } : {}),
169
+ ...(convertedInputSchema !== undefined
170
+ ? { inputSchema: convertedInputSchema }
171
+ : {}),
172
+ ...(toolOptions?.annotations !== undefined
173
+ ? { annotations: toolOptions.annotations }
174
+ : {}),
175
+ async execute(
176
+ input: object,
177
+ executionOptions?: { signal?: AbortSignal },
178
+ ) {
179
+ const validatedInput = await validateSchemaValue(inputSchema, input)
180
+ const output = await execute(validatedInput, {
181
+ abortSignal: executionOptions?.signal,
182
+ context: options.context,
183
+ emitCustomEvent() {},
184
+ })
185
+ return validateSchemaValue(outputSchema, output)
186
+ },
187
+ }
188
+ })
189
+
190
+ const registrationController = new AbortController()
191
+ const abortRegistration = () =>
192
+ registrationController.abort(options.signal.reason)
193
+
194
+ if (options.signal.aborted) {
195
+ abortRegistration()
196
+ } else {
197
+ options.signal.addEventListener('abort', abortRegistration, { once: true })
198
+ }
199
+
200
+ try {
201
+ for (const tool of webMCPTools) {
202
+ await document.modelContext.registerTool(tool, {
203
+ signal: registrationController.signal,
204
+ })
205
+ }
206
+ } catch (error) {
207
+ registrationController.abort(error)
208
+ options.signal.removeEventListener('abort', abortRegistration)
209
+ throw error
210
+ }
211
+ }