@tanstack/ai-client 0.30.0 → 0.31.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/README.md +14 -13
- package/dist/esm/byok/client.d.ts +6 -1
- package/dist/esm/byok/client.js +15 -6
- package/dist/esm/byok/client.js.map +1 -1
- package/dist/esm/byok/passkey.js +16 -0
- package/dist/esm/byok/passkey.js.map +1 -1
- package/dist/esm/chat-client.d.ts +0 -2
- package/dist/esm/chat-client.js +2 -6
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.js +19 -16
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/web-mcp-tools.d.ts +49 -0
- package/dist/esm/web-mcp-tools.js +80 -0
- package/dist/esm/web-mcp-tools.js.map +1 -0
- package/package.json +3 -3
- package/src/byok/client.ts +28 -7
- package/src/byok/passkey.ts +38 -0
- package/src/chat-client.ts +2 -10
- package/src/connection-adapters.ts +23 -19
- package/src/index.ts +7 -0
- package/src/web-mcp-tools.ts +211 -0
|
@@ -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
|
+
}
|