@opentiny/next-sdk 0.1.0 → 0.1.1-alpha.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.
@@ -0,0 +1,555 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
2
+ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
3
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
4
+ import { z, ZodObject, ZodLiteral, ZodType } from 'zod'
5
+ import {
6
+ ElicitRequestSchema,
7
+ CallToolResultSchema,
8
+ ListRootsRequestSchema,
9
+ CreateMessageRequestSchema,
10
+ LoggingMessageNotificationSchema,
11
+ ToolListChangedNotificationSchema,
12
+ ResourceUpdatedNotificationSchema,
13
+ PromptListChangedNotificationSchema,
14
+ ResourceListChangedNotificationSchema
15
+ } from '@modelcontextprotocol/sdk/types.js'
16
+ import {
17
+ MessageChannelClientTransport,
18
+ sseOptions,
19
+ streamOptions,
20
+ attemptConnection,
21
+ createStreamProxy,
22
+ createSseProxy,
23
+ AuthClientProvider
24
+ } from '@opentiny/next'
25
+ import type {
26
+ Result,
27
+ Request,
28
+ Notification,
29
+ Implementation,
30
+ ServerCapabilities,
31
+ ClientCapabilities,
32
+ LoggingLevel,
33
+ CompleteRequest,
34
+ CallToolRequest,
35
+ ListToolsRequest,
36
+ GetPromptRequest,
37
+ SubscribeRequest,
38
+ UnsubscribeRequest,
39
+ ListPromptsRequest,
40
+ ReadResourceRequest,
41
+ ListResourcesRequest,
42
+ ListResourceTemplatesRequest
43
+ } from '@modelcontextprotocol/sdk/types.js'
44
+ import type { ProxyOptions } from '@opentiny/next'
45
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
46
+ import type { ClientOptions } from '@modelcontextprotocol/sdk/client/index.js'
47
+ import type { SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js'
48
+ import type { StreamableHTTPClientTransportOptions } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
49
+ import type {
50
+ RequestOptions,
51
+ NotificationOptions,
52
+ RequestHandlerExtra
53
+ } from '@modelcontextprotocol/sdk/shared/protocol.js'
54
+ import { dynamicTool, jsonSchema, Tool, ToolCallOptions, ToolSet } from 'ai'
55
+
56
+ /**
57
+ * Options for configuring the server transport.
58
+ */
59
+ export interface ClientConnectOptions {
60
+ url: string
61
+ token?: string
62
+ sessionId?: string
63
+ authProvider?: AuthClientProvider
64
+ type?: 'channel' | 'sse'
65
+ agent?: boolean
66
+ onError?: (error: Error) => void
67
+ onUnauthorized?: (connect: () => Promise<void>) => Promise<void>
68
+ onReconnect?: () => Promise<void>
69
+ }
70
+
71
+ type SendRequestT = Request
72
+ type SendNotificationT = Notification
73
+ type SendResultT = Result
74
+
75
+ /**
76
+ * An MCP client on top of a pluggable transport.
77
+ * The client will automatically begin the initialization flow with the server when connect() is called.
78
+ */
79
+ export class WebMcpClient {
80
+ public readonly client: Client
81
+ public transport: Transport | undefined
82
+
83
+ constructor(clientInfo: Implementation, options?: ClientOptions) {
84
+ const info: Implementation = {
85
+ name: 'web-mcp-client',
86
+ version: '1.0.0'
87
+ }
88
+
89
+ const capabilities: ClientCapabilities = {
90
+ roots: { listChanged: true },
91
+ sampling: {},
92
+ elicitation: {}
93
+ }
94
+
95
+ this.client = new Client(clientInfo || info, options || { capabilities })
96
+
97
+ this.client.onclose = () => {
98
+ this.onclose?.()
99
+ }
100
+
101
+ this.client.onerror = (error: Error) => {
102
+ this.onerror?.(error)
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Connects the client to a transport via the specified option.
108
+ */
109
+ async connect(options: Transport | ClientConnectOptions): Promise<{ transport: Transport; sessionId: string }> {
110
+ if (typeof (options as Transport)['start'] === 'function') {
111
+ this.transport = options as Transport
112
+ this.transport.onclose = undefined
113
+ this.transport.onerror = undefined
114
+ this.transport.onmessage = undefined
115
+ await this.client.connect(this.transport)
116
+ return { transport: this.transport, sessionId: this.transport.sessionId as string }
117
+ }
118
+
119
+ const { url, token, sessionId, authProvider, type, agent, onError, onUnauthorized, onReconnect } =
120
+ options as ClientConnectOptions
121
+
122
+ if (agent === true) {
123
+ const proxyOptions: ProxyOptions = { client: this.client, url, token, sessionId, authProvider }
124
+
125
+ let reconnect = false
126
+ let response
127
+
128
+ const connectProxy = async () => {
129
+ const { transport, sessionId } =
130
+ type === 'sse' ? await createSseProxy(proxyOptions) : await createStreamProxy(proxyOptions)
131
+
132
+ transport.onerror = async (error: Error) => {
133
+ onError?.(error)
134
+
135
+ if (error.message === 'Unauthorized' && !reconnect) {
136
+ if (typeof onUnauthorized === 'function') {
137
+ await onUnauthorized(connectProxy)
138
+ } else {
139
+ reconnect = true
140
+ await connectProxy()
141
+ reconnect = false
142
+ await onReconnect?.()
143
+ }
144
+ }
145
+ }
146
+
147
+ response = { transport, sessionId }
148
+ }
149
+
150
+ await connectProxy()
151
+ return response as unknown as { transport: Transport; sessionId: string }
152
+ }
153
+
154
+ const endpoint = new URL(url)
155
+ let transport: Transport | undefined
156
+
157
+ if (type === 'channel') {
158
+ transport = new MessageChannelClientTransport(url)
159
+ await this.client.connect(transport)
160
+ }
161
+
162
+ if (type === 'sse') {
163
+ if (authProvider) {
164
+ const createTransport = () => new SSEClientTransport(endpoint, { authProvider })
165
+ transport = await attemptConnection(this.client, authProvider.waitForOAuthCode, createTransport)
166
+ } else {
167
+ const opts = sseOptions(token, sessionId) as SSEClientTransportOptions
168
+ transport = new SSEClientTransport(endpoint, opts)
169
+ await this.client.connect(transport)
170
+ }
171
+ }
172
+
173
+ if (typeof transport === 'undefined') {
174
+ if (authProvider) {
175
+ const createTransport = () => new StreamableHTTPClientTransport(endpoint, { authProvider })
176
+ transport = await attemptConnection(this.client, authProvider.waitForOAuthCode, createTransport)
177
+ } else {
178
+ const opts = streamOptions(token, sessionId) as StreamableHTTPClientTransportOptions
179
+ transport = new StreamableHTTPClientTransport(endpoint, opts)
180
+ await this.client.connect(transport)
181
+ }
182
+ }
183
+
184
+ this.transport = transport
185
+ return { transport: this.transport, sessionId: this.transport.sessionId as string }
186
+ }
187
+
188
+ /**
189
+ * Callback for when the connection is closed for any reason.
190
+ *
191
+ * This is invoked when close() is called as well.
192
+ */
193
+ onclose?: () => void
194
+
195
+ /**
196
+ * Callback for when an error occurs.
197
+ *
198
+ * Note that errors are not necessarily fatal; they are used for reporting any kind of exceptional condition out of band.
199
+ */
200
+ onerror?: (error: Error) => void
201
+
202
+ /**
203
+ * Closes the connection.
204
+ */
205
+ async close(): Promise<void> {
206
+ await this.client.close()
207
+ }
208
+
209
+ /**
210
+ * After initialization has completed, this will be populated with the server's reported capabilities.
211
+ */
212
+ getServerCapabilities(): ServerCapabilities | undefined {
213
+ return this.client.getServerCapabilities()
214
+ }
215
+
216
+ /**
217
+ * After initialization has completed, this will be populated with information about the server's name and version.
218
+ */
219
+ getServerVersion(): Implementation | undefined {
220
+ return this.client.getServerVersion()
221
+ }
222
+
223
+ /**
224
+ * After initialization has completed, this may be populated with information about the server's instructions.
225
+ */
226
+ getInstructions(): string | undefined {
227
+ return this.client.getInstructions()
228
+ }
229
+
230
+ /**
231
+ * Sends a ping to the server to check if it is still connected.
232
+ */
233
+ async ping(options?: RequestOptions) {
234
+ return await this.client.ping(options)
235
+ }
236
+
237
+ /**
238
+ * Sends a completion request to the server.
239
+ */
240
+ async complete(params: CompleteRequest['params'], options?: RequestOptions) {
241
+ return await this.client.complete(params, options)
242
+ }
243
+
244
+ /**
245
+ * Sends a request for setting the logging level to the server.
246
+ */
247
+ async setLoggingLevel(level: LoggingLevel, options?: RequestOptions) {
248
+ return await this.client.setLoggingLevel(level, options)
249
+ }
250
+
251
+ /**
252
+ * Gets the prompt with the given params from the server.
253
+ */
254
+ async getPrompt(params: GetPromptRequest['params'], options?: RequestOptions) {
255
+ return await this.client.getPrompt(params, options)
256
+ }
257
+
258
+ /**
259
+ * Lists all prompts available on the server.
260
+ */
261
+ async listPrompts(params?: ListPromptsRequest['params'], options?: RequestOptions) {
262
+ return await this.client.listPrompts(params, options)
263
+ }
264
+
265
+ /**
266
+ * Lists all resources available on the server.
267
+ */
268
+ async listResources(params?: ListResourcesRequest['params'], options?: RequestOptions) {
269
+ return await this.client.listResources(params, options)
270
+ }
271
+
272
+ /**
273
+ * Lists all resource templates available on the server.
274
+ */
275
+ async listResourceTemplates(params?: ListResourceTemplatesRequest['params'], options?: RequestOptions) {
276
+ return await this.client.listResourceTemplates(params, options)
277
+ }
278
+
279
+ /**
280
+ * Reads the resource with the given params from the server.
281
+ */
282
+ async readResource(params: ReadResourceRequest['params'], options?: RequestOptions) {
283
+ return await this.client.readResource(params, options)
284
+ }
285
+
286
+ /**
287
+ * Subscribes to a resource on the server.
288
+ */
289
+ async subscribeResource(params: SubscribeRequest['params'], options?: RequestOptions) {
290
+ return await this.client.subscribeResource(params, options)
291
+ }
292
+
293
+ /**
294
+ * Unsubscribes from a resource on the server.
295
+ */
296
+ async unsubscribeResource(params: UnsubscribeRequest['params'], options?: RequestOptions) {
297
+ return await this.client.unsubscribeResource(params, options)
298
+ }
299
+
300
+ /**
301
+ * Calls a tool on the server with the given parameters.
302
+ */
303
+ async callTool(params: CallToolRequest['params'], options?: RequestOptions) {
304
+ return await this.client.callTool(params, CallToolResultSchema, options)
305
+ }
306
+
307
+ /**
308
+ * Lists all tools available on the server.
309
+ */
310
+ async listTools(params?: ListToolsRequest['params'], options?: RequestOptions) {
311
+ return await this.client.listTools(params, options)
312
+ }
313
+
314
+ /**
315
+ * Returns a set of AI SDK tools from the MCP server
316
+ * @returns A record of tool names to their implementations
317
+ */
318
+ async tools(params?: ListToolsRequest['params'], options?: RequestOptions): Promise<ToolSet> {
319
+ const tools: Record<string, Tool> = {}
320
+
321
+ try {
322
+ const listToolsResult = await this.listTools(params, options)
323
+
324
+ for (const { name, description, inputSchema } of listToolsResult.tools) {
325
+ const execute = async (args: any, options: ToolCallOptions): Promise<any> => {
326
+ return this.callTool({ name, arguments: args }, { signal: options?.abortSignal })
327
+ }
328
+
329
+ tools[name] = dynamicTool({
330
+ description,
331
+ inputSchema: jsonSchema({
332
+ ...inputSchema,
333
+ properties: inputSchema.properties ?? {},
334
+ additionalProperties: false
335
+ }),
336
+ execute
337
+ })
338
+ }
339
+
340
+ return tools
341
+ } catch (error) {
342
+ throw error
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Sends a notification for the roots list changed event to the server.
348
+ */
349
+ async sendRootsListChanged() {
350
+ return await this.client.sendRootsListChanged()
351
+ }
352
+
353
+ /**
354
+ * Sends a request and wait for a response.
355
+ *
356
+ * Do not use this method to emit notifications! Use notification() instead.
357
+ */
358
+ request<T extends ZodType<object>>(
359
+ request: SendRequestT,
360
+ resultSchema: T,
361
+ options?: RequestOptions
362
+ ): Promise<z.infer<T>> {
363
+ return this.client.request(request, resultSchema, options)
364
+ }
365
+
366
+ /**
367
+ * Emits a notification, which is a one-way message that does not expect a response.
368
+ */
369
+ async notification(notification: SendNotificationT, options?: NotificationOptions): Promise<void> {
370
+ return await this.client.notification(notification, options)
371
+ }
372
+
373
+ /**
374
+ * Registers a handler to invoke when this protocol object receives a request with the given method.
375
+ *
376
+ * Note that this will replace any previous request handler for the same method.
377
+ */
378
+ setRequestHandler<
379
+ T extends ZodObject<{
380
+ method: ZodLiteral<string>
381
+ }>
382
+ >(
383
+ requestSchema: T,
384
+ handler: (
385
+ request: z.infer<T>,
386
+ extra: RequestHandlerExtra<SendRequestT, SendNotificationT>
387
+ ) => SendResultT | Promise<SendResultT>
388
+ ): void {
389
+ this.client.setRequestHandler(requestSchema, handler)
390
+ }
391
+
392
+ /**
393
+ * Removes the request handler for the given method.
394
+ */
395
+ removeRequestHandler(method: string): void {
396
+ this.client.removeRequestHandler(method)
397
+ }
398
+
399
+ /**
400
+ * Registers a handler to invoke when this protocol object receives a notification with the given method.
401
+ *
402
+ * Note that this will replace any previous notification handler for the same method.
403
+ */
404
+ setNotificationHandler<
405
+ T extends ZodObject<{
406
+ method: ZodLiteral<string>
407
+ }>
408
+ >(notificationSchema: T, handler: (notification: z.infer<T>) => void | Promise<void>): void {
409
+ this.client.setNotificationHandler(notificationSchema, handler)
410
+ }
411
+
412
+ /**
413
+ * Removes the notification handler for the given method.
414
+ */
415
+ removeNotificationHandler(method: string): void {
416
+ this.client.removeNotificationHandler(method)
417
+ }
418
+
419
+ /**
420
+ * Registers a handler for the elicitation request.
421
+ */
422
+ onElicit(
423
+ handler: (
424
+ request: z.infer<typeof ElicitRequestSchema>,
425
+ extra: RequestHandlerExtra<SendRequestT, SendNotificationT>
426
+ ) => SendResultT | Promise<SendResultT>
427
+ ): void {
428
+ this.client.setRequestHandler(ElicitRequestSchema, handler)
429
+ }
430
+
431
+ /**
432
+ * Registers a handler for the create LLM message request.
433
+ */
434
+ onCreateMessage(
435
+ handler: (
436
+ request: z.infer<typeof CreateMessageRequestSchema>,
437
+ extra: RequestHandlerExtra<SendRequestT, SendNotificationT>
438
+ ) => SendResultT | Promise<SendResultT>
439
+ ): void {
440
+ this.client.setRequestHandler(CreateMessageRequestSchema, handler)
441
+ }
442
+
443
+ /**
444
+ * Registers a handler for the list roots request.
445
+ */
446
+ onListRoots(
447
+ handler: (
448
+ request: z.infer<typeof ListRootsRequestSchema>,
449
+ extra: RequestHandlerExtra<SendRequestT, SendNotificationT>
450
+ ) => SendResultT | Promise<SendResultT>
451
+ ): void {
452
+ this.client.setRequestHandler(ListRootsRequestSchema, handler)
453
+ }
454
+
455
+ /**
456
+ * Registers a handler for the tool list changed notification.
457
+ */
458
+ onToolListChanged(
459
+ handler: (notification: z.infer<typeof ToolListChangedNotificationSchema>) => void | Promise<void>
460
+ ): void {
461
+ this.client.setNotificationHandler(ToolListChangedNotificationSchema, handler)
462
+ }
463
+
464
+ /**
465
+ * Registers a handler for the prompt list changed notification.
466
+ */
467
+ onPromptListChanged(
468
+ handler: (notification: z.infer<typeof PromptListChangedNotificationSchema>) => void | Promise<void>
469
+ ): void {
470
+ this.client.setNotificationHandler(PromptListChangedNotificationSchema, handler)
471
+ }
472
+
473
+ /**
474
+ * Registers a handler for the resource list changed notification.
475
+ */
476
+ onResourceListChanged(
477
+ handler: (notification: z.infer<typeof ResourceListChangedNotificationSchema>) => void | Promise<void>
478
+ ): void {
479
+ this.client.setNotificationHandler(ResourceListChangedNotificationSchema, handler)
480
+ }
481
+
482
+ /**
483
+ * Registers a handler for the resource updated notification.
484
+ */
485
+ onResourceUpdated(
486
+ handler: (notification: z.infer<typeof ResourceUpdatedNotificationSchema>) => void | Promise<void>
487
+ ): void {
488
+ this.client.setNotificationHandler(ResourceUpdatedNotificationSchema, handler)
489
+ }
490
+
491
+ /**
492
+ * Registers a handler for the logging message notification.
493
+ */
494
+ onLoggingMessage(
495
+ handler: (notification: z.infer<typeof LoggingMessageNotificationSchema>) => void | Promise<void>
496
+ ): void {
497
+ this.client.setNotificationHandler(LoggingMessageNotificationSchema, handler)
498
+ }
499
+
500
+ /**
501
+ * Close the transport for window.addEventListener('pagehide')
502
+ */
503
+ async onPagehide(event: PageTransitionEvent) {
504
+ if (event.persisted) {
505
+ return
506
+ }
507
+
508
+ if (isStreamableHTTPClientTransport(this.transport)) {
509
+ await this.transport.terminateSession()
510
+ } else if (this.transport && typeof this.transport['close'] === 'function') {
511
+ await this.transport.close()
512
+ }
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Creates a new SSEClientTransport instance.
518
+ */
519
+ export const createSSEClientTransport = (url: URL, opts?: SSEClientTransportOptions) =>
520
+ new SSEClientTransport(url, opts)
521
+
522
+ /**
523
+ * Creates a new StreamableHTTPClientTransport instance.
524
+ */
525
+ export const createStreamableHTTPClientTransport = (url: URL, opts?: StreamableHTTPClientTransportOptions) =>
526
+ new StreamableHTTPClientTransport(url, opts)
527
+
528
+ /**
529
+ * Creates a new MessageChannelClientTransport instance.
530
+ */
531
+ export const createMessageChannelClientTransport = (endpoint: string, globalObject?: object) =>
532
+ new MessageChannelClientTransport(endpoint, globalObject)
533
+
534
+ /**
535
+ * Checks if the transport is a SSEClientTransport.
536
+ */
537
+ export const isSSEClientTransport = (transport: unknown): transport is SSEClientTransport =>
538
+ transport instanceof SSEClientTransport
539
+
540
+ /**
541
+ * Checks if the transport is a StreamableHTTPClientTransport.
542
+ */
543
+ export const isStreamableHTTPClientTransport = (transport: unknown): transport is StreamableHTTPClientTransport =>
544
+ transport instanceof StreamableHTTPClientTransport
545
+
546
+ /**
547
+ * Checks if the transport is a MessageChannelClientTransport.
548
+ */
549
+ export const isMessageChannelClientTransport = (transport: unknown): transport is MessageChannelClientTransport =>
550
+ transport instanceof MessageChannelClientTransport
551
+
552
+ /**
553
+ * Checks if the client is an instance of MCP Client.
554
+ */
555
+ export const isMcpClient = (client: unknown): client is Client => client instanceof Client