agents 0.2.2 → 0.2.4

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.
@@ -1,15 +1,72 @@
1
1
  // src/mcp/client.ts
2
2
  import { jsonSchema } from "ai";
3
- import { nanoid } from "nanoid";
3
+ import { nanoid as nanoid2 } from "nanoid";
4
+
5
+ // src/core/events.ts
6
+ function toDisposable(fn) {
7
+ return { dispose: fn };
8
+ }
9
+ var DisposableStore = class {
10
+ constructor() {
11
+ this._items = [];
12
+ }
13
+ add(d) {
14
+ this._items.push(d);
15
+ return d;
16
+ }
17
+ dispose() {
18
+ while (this._items.length) {
19
+ try {
20
+ this._items.pop().dispose();
21
+ } catch {
22
+ }
23
+ }
24
+ }
25
+ };
26
+ var Emitter = class {
27
+ constructor() {
28
+ this._listeners = /* @__PURE__ */ new Set();
29
+ this.event = (listener) => {
30
+ this._listeners.add(listener);
31
+ return toDisposable(() => this._listeners.delete(listener));
32
+ };
33
+ }
34
+ fire(data) {
35
+ for (const listener of [...this._listeners]) {
36
+ try {
37
+ listener(data);
38
+ } catch (err) {
39
+ console.error("Emitter listener error:", err);
40
+ }
41
+ }
42
+ }
43
+ dispose() {
44
+ this._listeners.clear();
45
+ }
46
+ };
4
47
 
5
48
  // src/mcp/client-connection.ts
6
49
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
50
  import {
51
+ ElicitRequestSchema,
8
52
  PromptListChangedNotificationSchema,
9
53
  ResourceListChangedNotificationSchema,
10
- ToolListChangedNotificationSchema,
11
- ElicitRequestSchema
54
+ ToolListChangedNotificationSchema
12
55
  } from "@modelcontextprotocol/sdk/types.js";
56
+ import { nanoid } from "nanoid";
57
+
58
+ // src/mcp/errors.ts
59
+ function toErrorMessage(error) {
60
+ return error instanceof Error ? error.message : String(error);
61
+ }
62
+ function isUnauthorized(error) {
63
+ const msg = toErrorMessage(error);
64
+ return msg.includes("Unauthorized") || msg.includes("401");
65
+ }
66
+ function isTransportNotImplemented(error) {
67
+ const msg = toErrorMessage(error);
68
+ return msg.includes("404") || msg.includes("405") || msg.includes("Not Implemented") || msg.includes("not implemented");
69
+ }
13
70
 
14
71
  // src/mcp/sse-edge.ts
15
72
  import {
@@ -118,7 +175,8 @@ var MCPClientConnection = class {
118
175
  this.prompts = [];
119
176
  this.resources = [];
120
177
  this.resourceTemplates = [];
121
- this.url = url;
178
+ this._onObservabilityEvent = new Emitter();
179
+ this.onObservabilityEvent = this._onObservabilityEvent.event;
122
180
  const clientOptions = {
123
181
  ...options.client,
124
182
  capabilities: {
@@ -131,23 +189,112 @@ var MCPClientConnection = class {
131
189
  /**
132
190
  * Initialize a client connection
133
191
  *
134
- * @param code Optional OAuth code to initialize the connection with if auth hasn't been initialized
135
192
  * @returns
136
193
  */
137
- async init(code) {
194
+ async init() {
195
+ const transportType = this.options.transport.type;
196
+ if (!transportType) {
197
+ throw new Error("Transport type must be specified");
198
+ }
138
199
  try {
139
- const transportType = this.options.transport.type || "streamable-http";
140
- await this.tryConnect(transportType, code);
200
+ await this.tryConnect(transportType);
141
201
  } catch (e) {
142
- if (e.toString().includes("Unauthorized")) {
202
+ if (isUnauthorized(e)) {
143
203
  this.connectionState = "authenticating";
144
204
  return;
145
205
  }
206
+ this._onObservabilityEvent.fire({
207
+ type: "mcp:client:connect",
208
+ displayMessage: `Connection initialization failed for ${this.url.toString()}`,
209
+ payload: {
210
+ url: this.url.toString(),
211
+ transport: transportType,
212
+ state: this.connectionState,
213
+ error: toErrorMessage(e)
214
+ },
215
+ timestamp: Date.now(),
216
+ id: nanoid()
217
+ });
146
218
  this.connectionState = "failed";
219
+ return;
220
+ }
221
+ await this.discoverAndRegister();
222
+ }
223
+ /**
224
+ * Finish OAuth by probing transports based on configured type.
225
+ * - Explicit: finish on that transport
226
+ * - Auto: try streamable-http, then sse on 404/405/Not Implemented
227
+ */
228
+ async finishAuthProbe(code) {
229
+ if (!this.options.transport.authProvider) {
230
+ throw new Error("No auth provider configured");
231
+ }
232
+ const configuredType = this.options.transport.type;
233
+ if (!configuredType) {
234
+ throw new Error("Transport type must be specified");
235
+ }
236
+ const finishAuth = async (base) => {
237
+ const transport = this.getTransport(base);
238
+ await transport.finishAuth(code);
239
+ };
240
+ if (configuredType === "sse" || configuredType === "streamable-http") {
241
+ await finishAuth(configuredType);
242
+ return;
243
+ }
244
+ try {
245
+ await finishAuth("streamable-http");
246
+ } catch (e) {
247
+ if (isTransportNotImplemented(e)) {
248
+ await finishAuth("sse");
249
+ return;
250
+ }
147
251
  throw e;
148
252
  }
253
+ }
254
+ /**
255
+ * Complete OAuth authorization
256
+ */
257
+ async completeAuthorization(code) {
258
+ if (this.connectionState !== "authenticating") {
259
+ throw new Error(
260
+ "Connection must be in authenticating state to complete authorization"
261
+ );
262
+ }
263
+ try {
264
+ await this.finishAuthProbe(code);
265
+ this.connectionState = "connecting";
266
+ } catch (error) {
267
+ this.connectionState = "failed";
268
+ throw error;
269
+ }
270
+ }
271
+ /**
272
+ * Establish connection after successful authorization
273
+ */
274
+ async establishConnection() {
275
+ if (this.connectionState !== "connecting") {
276
+ throw new Error(
277
+ "Connection must be in connecting state to establish connection"
278
+ );
279
+ }
280
+ try {
281
+ const transportType = this.options.transport.type;
282
+ if (!transportType) {
283
+ throw new Error("Transport type must be specified");
284
+ }
285
+ await this.tryConnect(transportType);
286
+ await this.discoverAndRegister();
287
+ } catch (error) {
288
+ this.connectionState = "failed";
289
+ throw error;
290
+ }
291
+ }
292
+ /**
293
+ * Discover server capabilities and register tools, resources, prompts, and templates
294
+ */
295
+ async discoverAndRegister() {
149
296
  this.connectionState = "discovering";
150
- this.serverCapabilities = await this.client.getServerCapabilities();
297
+ this.serverCapabilities = this.client.getServerCapabilities();
151
298
  if (!this.serverCapabilities) {
152
299
  throw new Error("The MCP Server failed to return server capabilities");
153
300
  }
@@ -173,7 +320,18 @@ var MCPClientConnection = class {
173
320
  ];
174
321
  for (const { name, result } of operations) {
175
322
  if (result.status === "rejected") {
176
- console.error(`Failed to initialize ${name}:`, result.reason);
323
+ const url = this.url.toString();
324
+ this._onObservabilityEvent.fire({
325
+ type: "mcp:client:discover",
326
+ displayMessage: `Failed to discover ${name} for ${url}`,
327
+ payload: {
328
+ url,
329
+ capability: name,
330
+ error: result.reason
331
+ },
332
+ timestamp: Date.now(),
333
+ id: nanoid()
334
+ });
177
335
  }
178
336
  }
179
337
  this.instructions = instructionsResult.status === "fulfilled" ? instructionsResult.value : void 0;
@@ -240,7 +398,7 @@ var MCPClientConnection = class {
240
398
  do {
241
399
  toolsResult = await this.client.listTools({
242
400
  cursor: toolsResult.nextCursor
243
- }).catch(capabilityErrorHandler({ tools: [] }, "tools/list"));
401
+ }).catch(this._capabilityErrorHandler({ tools: [] }, "tools/list"));
244
402
  toolsAgg = toolsAgg.concat(toolsResult.tools);
245
403
  } while (toolsResult.nextCursor);
246
404
  return toolsAgg;
@@ -251,7 +409,9 @@ var MCPClientConnection = class {
251
409
  do {
252
410
  resourcesResult = await this.client.listResources({
253
411
  cursor: resourcesResult.nextCursor
254
- }).catch(capabilityErrorHandler({ resources: [] }, "resources/list"));
412
+ }).catch(
413
+ this._capabilityErrorHandler({ resources: [] }, "resources/list")
414
+ );
255
415
  resourcesAgg = resourcesAgg.concat(resourcesResult.resources);
256
416
  } while (resourcesResult.nextCursor);
257
417
  return resourcesAgg;
@@ -262,7 +422,7 @@ var MCPClientConnection = class {
262
422
  do {
263
423
  promptsResult = await this.client.listPrompts({
264
424
  cursor: promptsResult.nextCursor
265
- }).catch(capabilityErrorHandler({ prompts: [] }, "prompts/list"));
425
+ }).catch(this._capabilityErrorHandler({ prompts: [] }, "prompts/list"));
266
426
  promptsAgg = promptsAgg.concat(promptsResult.prompts);
267
427
  } while (promptsResult.nextCursor);
268
428
  return promptsAgg;
@@ -276,7 +436,7 @@ var MCPClientConnection = class {
276
436
  templatesResult = await this.client.listResourceTemplates({
277
437
  cursor: templatesResult.nextCursor
278
438
  }).catch(
279
- capabilityErrorHandler(
439
+ this._capabilityErrorHandler(
280
440
  { resourceTemplates: [] },
281
441
  "resources/templates/list"
282
442
  )
@@ -315,37 +475,46 @@ var MCPClientConnection = class {
315
475
  throw new Error(`Unsupported transport type: ${transportType}`);
316
476
  }
317
477
  }
318
- async tryConnect(transportType, code) {
319
- let effectiveTransportType = transportType;
320
- if (code && this.options.transport.authProvider) {
321
- const savedTransport = await this.options.transport.authProvider.getOAuthTransport();
322
- if (savedTransport) {
323
- effectiveTransportType = savedTransport;
324
- }
325
- }
326
- const transports = effectiveTransportType === "auto" ? ["streamable-http", "sse"] : [effectiveTransportType];
478
+ async tryConnect(transportType) {
479
+ const transports = transportType === "auto" ? ["streamable-http", "sse"] : [transportType];
327
480
  for (const currentTransportType of transports) {
328
481
  const isLastTransport = currentTransportType === transports[transports.length - 1];
329
- const hasFallback = effectiveTransportType === "auto" && currentTransportType === "streamable-http" && !isLastTransport;
482
+ const hasFallback = transportType === "auto" && currentTransportType === "streamable-http" && !isLastTransport;
330
483
  const transport = this.getTransport(currentTransportType);
331
- if (code) {
332
- await transport.finishAuth(code);
333
- }
334
484
  try {
335
485
  await this.client.connect(transport);
336
- if (code && this.options.transport.authProvider) {
337
- await this.options.transport.authProvider.clearOAuthTransport();
338
- }
486
+ this.lastConnectedTransport = currentTransportType;
487
+ const url = this.url.toString();
488
+ this._onObservabilityEvent.fire({
489
+ type: "mcp:client:connect",
490
+ displayMessage: `Connected successfully using ${currentTransportType} transport for ${url}`,
491
+ payload: {
492
+ url,
493
+ transport: currentTransportType,
494
+ state: this.connectionState
495
+ },
496
+ timestamp: Date.now(),
497
+ id: nanoid()
498
+ });
339
499
  break;
340
500
  } catch (e) {
341
501
  const error = e instanceof Error ? e : new Error(String(e));
342
- if (!code && error.message.includes("Unauthorized") && this.options.transport.authProvider && currentTransportType) {
343
- await this.options.transport.authProvider.saveOAuthTransport(
344
- currentTransportType
345
- );
502
+ if (isUnauthorized(error)) {
346
503
  throw e;
347
504
  }
348
- if (hasFallback && (error.message.includes("404") || error.message.includes("405"))) {
505
+ if (hasFallback && isTransportNotImplemented(error)) {
506
+ const url = this.url.toString();
507
+ this._onObservabilityEvent.fire({
508
+ type: "mcp:client:connect",
509
+ displayMessage: `${currentTransportType} transport not available, trying ${transports[transports.indexOf(currentTransportType) + 1]} for ${url}`,
510
+ payload: {
511
+ url,
512
+ transport: currentTransportType,
513
+ state: this.connectionState
514
+ },
515
+ timestamp: Date.now(),
516
+ id: nanoid()
517
+ });
349
518
  continue;
350
519
  }
351
520
  throw e;
@@ -358,18 +527,27 @@ var MCPClientConnection = class {
358
527
  }
359
528
  );
360
529
  }
530
+ _capabilityErrorHandler(empty, method) {
531
+ return (e) => {
532
+ if (e.code === -32601) {
533
+ const url = this.url.toString();
534
+ this._onObservabilityEvent.fire({
535
+ type: "mcp:client:discover",
536
+ displayMessage: `The server advertised support for the capability ${method.split("/")[0]}, but returned "Method not found" for '${method}' for ${url}`,
537
+ payload: {
538
+ url,
539
+ capability: method.split("/")[0],
540
+ error: toErrorMessage(e)
541
+ },
542
+ timestamp: Date.now(),
543
+ id: nanoid()
544
+ });
545
+ return empty;
546
+ }
547
+ throw e;
548
+ };
549
+ }
361
550
  };
362
- function capabilityErrorHandler(empty, method) {
363
- return (e) => {
364
- if (e.code === -32601) {
365
- console.error(
366
- `The server advertised support for the capability ${method.split("/")[0]}, but returned "Method not found" for '${method}'.`
367
- );
368
- return empty;
369
- }
370
- throw e;
371
- };
372
- }
373
551
 
374
552
  // src/mcp/client.ts
375
553
  var MCPClientManager = class {
@@ -384,6 +562,11 @@ var MCPClientManager = class {
384
562
  this.mcpConnections = {};
385
563
  this._callbackUrls = [];
386
564
  this._didWarnAboutUnstableGetAITools = false;
565
+ this._connectionDisposables = /* @__PURE__ */ new Map();
566
+ this._onObservabilityEvent = new Emitter();
567
+ this.onObservabilityEvent = this._onObservabilityEvent.event;
568
+ this._onConnected = new Emitter();
569
+ this.onConnected = this._onConnected.event;
387
570
  }
388
571
  /**
389
572
  * Connect to and register an MCP server
@@ -393,18 +576,18 @@ var MCPClientManager = class {
393
576
  * @param capabilities Client capabilities (i.e. if the client supports roots/sampling)
394
577
  */
395
578
  async connect(url, options = {}) {
396
- const id = options.reconnect?.id ?? nanoid(8);
397
- if (!options.transport?.authProvider) {
398
- console.warn(
399
- "No authProvider provided in the transport options. This client will only support unauthenticated remote MCP Servers"
400
- );
401
- } else {
579
+ const id = options.reconnect?.id ?? nanoid2(8);
580
+ if (options.transport?.authProvider) {
402
581
  options.transport.authProvider.serverId = id;
403
582
  if (options.reconnect?.oauthClientId) {
404
583
  options.transport.authProvider.clientId = options.reconnect?.oauthClientId;
405
584
  }
406
585
  }
407
586
  if (!options.reconnect?.oauthCode || !this.mcpConnections[id]) {
587
+ const normalizedTransport = {
588
+ ...options.transport,
589
+ type: options.transport?.type ?? "auto"
590
+ };
408
591
  this.mcpConnections[id] = new MCPClientConnection(
409
592
  new URL(url),
410
593
  {
@@ -413,13 +596,44 @@ var MCPClientManager = class {
413
596
  },
414
597
  {
415
598
  client: options.client ?? {},
416
- transport: options.transport ?? {}
599
+ transport: normalizedTransport
417
600
  }
418
601
  );
602
+ const store = new DisposableStore();
603
+ const existing = this._connectionDisposables.get(id);
604
+ if (existing) existing.dispose();
605
+ this._connectionDisposables.set(id, store);
606
+ store.add(
607
+ this.mcpConnections[id].onObservabilityEvent((event) => {
608
+ this._onObservabilityEvent.fire(event);
609
+ })
610
+ );
611
+ }
612
+ await this.mcpConnections[id].init();
613
+ if (options.reconnect?.oauthCode) {
614
+ try {
615
+ await this.mcpConnections[id].completeAuthorization(
616
+ options.reconnect.oauthCode
617
+ );
618
+ await this.mcpConnections[id].establishConnection();
619
+ } catch (error) {
620
+ this._onObservabilityEvent.fire({
621
+ type: "mcp:client:connect",
622
+ displayMessage: `Failed to complete OAuth reconnection for ${id} for ${url}`,
623
+ payload: {
624
+ url,
625
+ transport: options.transport?.type ?? "auto",
626
+ state: this.mcpConnections[id].connectionState,
627
+ error: toErrorMessage(error)
628
+ },
629
+ timestamp: Date.now(),
630
+ id
631
+ });
632
+ throw error;
633
+ }
419
634
  }
420
- await this.mcpConnections[id].init(options.reconnect?.oauthCode);
421
635
  const authUrl = options.transport?.authProvider?.authUrl;
422
- if (authUrl && options.transport?.authProvider?.redirectUrl) {
636
+ if (this.mcpConnections[id].connectionState === "authenticating" && authUrl && options.transport?.authProvider?.redirectUrl) {
423
637
  this._callbackUrls.push(
424
638
  options.transport.authProvider.redirectUrl.toString()
425
639
  );
@@ -474,19 +688,56 @@ var MCPClientManager = class {
474
688
  }
475
689
  conn.options.transport.authProvider.clientId = clientId;
476
690
  conn.options.transport.authProvider.serverId = serverId;
477
- const serverUrl = conn.url.toString();
478
- await this.connect(serverUrl, {
479
- reconnect: {
480
- id: serverId,
481
- oauthClientId: clientId,
482
- oauthCode: code
483
- },
484
- ...conn.options
485
- });
486
- if (this.mcpConnections[serverId].connectionState === "authenticating") {
487
- throw new Error("Failed to authenticate: client failed to initialize");
691
+ try {
692
+ await conn.completeAuthorization(code);
693
+ return {
694
+ serverId,
695
+ authSuccess: true
696
+ };
697
+ } catch (error) {
698
+ const errorMessage = error instanceof Error ? error.message : String(error);
699
+ return {
700
+ serverId,
701
+ authSuccess: false,
702
+ authError: errorMessage
703
+ };
704
+ }
705
+ }
706
+ /**
707
+ * Establish connection in the background after OAuth completion
708
+ * This method is called asynchronously and doesn't block the OAuth callback response
709
+ * @param serverId The server ID to establish connection for
710
+ */
711
+ async establishConnection(serverId) {
712
+ const conn = this.mcpConnections[serverId];
713
+ if (!conn) {
714
+ this._onObservabilityEvent.fire({
715
+ type: "mcp:client:preconnect",
716
+ displayMessage: `Connection not found for serverId: ${serverId}`,
717
+ payload: { serverId },
718
+ timestamp: Date.now(),
719
+ id: nanoid2()
720
+ });
721
+ return;
722
+ }
723
+ try {
724
+ await conn.establishConnection();
725
+ this._onConnected.fire(serverId);
726
+ } catch (error) {
727
+ const url = conn.url.toString();
728
+ this._onObservabilityEvent.fire({
729
+ type: "mcp:client:connect",
730
+ displayMessage: `Failed to establish connection to server ${serverId} with url ${url}`,
731
+ payload: {
732
+ url,
733
+ transport: conn.options.transport.type ?? "auto",
734
+ state: conn.connectionState,
735
+ error: toErrorMessage(error)
736
+ },
737
+ timestamp: Date.now(),
738
+ id: nanoid2()
739
+ });
488
740
  }
489
- return { serverId };
490
741
  }
491
742
  /**
492
743
  * Register a callback URL for OAuth handling
@@ -506,6 +757,20 @@ var MCPClientManager = class {
506
757
  (url) => !url.endsWith(`/${serverId}`)
507
758
  );
508
759
  }
760
+ /**
761
+ * Configure OAuth callback handling
762
+ * @param config OAuth callback configuration
763
+ */
764
+ configureOAuthCallback(config) {
765
+ this._oauthCallbackConfig = config;
766
+ }
767
+ /**
768
+ * Get the current OAuth callback configuration
769
+ * @returns The current OAuth callback configuration
770
+ */
771
+ getOAuthCallbackConfig() {
772
+ return this._oauthCallbackConfig;
773
+ }
509
774
  /**
510
775
  * @returns namespaced list of tools
511
776
  */
@@ -533,8 +798,12 @@ var MCPClientManager = class {
533
798
  }
534
799
  return result;
535
800
  },
801
+ // @ts-expect-error drift between ai and mcp types
536
802
  inputSchema: jsonSchema(tool.inputSchema),
537
- outputSchema: tool.outputSchema ? jsonSchema(tool.outputSchema) : void 0
803
+ outputSchema: tool.outputSchema ? (
804
+ // @ts-expect-error drift between ai and mcp types
805
+ jsonSchema(tool.outputSchema)
806
+ ) : void 0
538
807
  }
539
808
  ];
540
809
  })
@@ -557,11 +826,18 @@ var MCPClientManager = class {
557
826
  * Closes all connections to MCP servers
558
827
  */
559
828
  async closeAllConnections() {
560
- return Promise.all(
561
- Object.values(this.mcpConnections).map(async (connection) => {
562
- await connection.client.close();
829
+ const ids = Object.keys(this.mcpConnections);
830
+ await Promise.all(
831
+ ids.map(async (id) => {
832
+ await this.mcpConnections[id].client.close();
563
833
  })
564
834
  );
835
+ for (const id of ids) {
836
+ const store = this._connectionDisposables.get(id);
837
+ if (store) store.dispose();
838
+ this._connectionDisposables.delete(id);
839
+ delete this.mcpConnections[id];
840
+ }
565
841
  }
566
842
  /**
567
843
  * Closes a connection to an MCP server
@@ -573,6 +849,20 @@ var MCPClientManager = class {
573
849
  }
574
850
  await this.mcpConnections[id].client.close();
575
851
  delete this.mcpConnections[id];
852
+ const store = this._connectionDisposables.get(id);
853
+ if (store) store.dispose();
854
+ this._connectionDisposables.delete(id);
855
+ }
856
+ /**
857
+ * Dispose the manager and all resources.
858
+ */
859
+ async dispose() {
860
+ try {
861
+ await this.closeAllConnections();
862
+ } finally {
863
+ this._onConnected.dispose();
864
+ this._onObservabilityEvent.dispose();
865
+ }
576
866
  }
577
867
  /**
578
868
  * @returns namespaced list of prompts
@@ -642,9 +932,10 @@ function getNamespacedData(mcpClients, type) {
642
932
  }
643
933
 
644
934
  export {
935
+ DisposableStore,
645
936
  SSEEdgeClientTransport,
646
937
  StreamableHTTPEdgeClientTransport,
647
938
  MCPClientManager,
648
939
  getNamespacedData
649
940
  };
650
- //# sourceMappingURL=chunk-QEPGNUG6.js.map
941
+ //# sourceMappingURL=chunk-3OT2NNEW.js.map