agents 0.5.1 → 0.7.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.
@@ -1,6 +1,6 @@
1
1
  import { RetryOptions } from "../retries.js";
2
- import { d as Event, i as MCPTransportOptions, l as TransportType, n as MCPClientConnection, r as MCPConnectionState, t as MCPServerRow, u as Emitter } from "../client-storage-D633wI1S.js";
3
- import { t as MCPObservabilityEvent } from "../mcp-DA0kDE7K.js";
2
+ import { E as Event, T as Emitter, i as MCPTransportOptions, n as MCPClientConnection, r as MCPConnectionState, t as MCPServerRow, w as TransportType } from "../client-storage-tusTuoSF.js";
3
+ import { n as MCPObservabilityEvent } from "../agent-DnmmRjyv.js";
4
4
  import { AgentMcpOAuthProvider } from "./do-oauth-client-provider.js";
5
5
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
6
  import { CallToolRequest, CallToolResultSchema, CompatibilityCallToolResultSchema, GetPromptRequest, Prompt, ReadResourceRequest, Resource, ResourceTemplate, Tool } from "@modelcontextprotocol/sdk/types.js";
@@ -39,7 +39,7 @@ type MCPOAuthCallbackResult = {
39
39
  type RegisterServerOptions = {
40
40
  url: string;
41
41
  name: string;
42
- callbackUrl: string;
42
+ callbackUrl?: string;
43
43
  client?: ConstructorParameters<typeof Client>[1];
44
44
  transport?: MCPTransportOptions;
45
45
  authUrl?: string;
@@ -102,6 +102,7 @@ declare class MCPClientManager {
102
102
  private _storage;
103
103
  private _createAuthProviderFn?;
104
104
  private _isRestored;
105
+ private _pendingConnections;
105
106
  /** @internal Protected for testing purposes. */
106
107
  protected readonly _onObservabilityEvent: Emitter<MCPObservabilityEvent>;
107
108
  readonly onObservabilityEvent: Event<MCPObservabilityEvent>;
@@ -133,13 +134,46 @@ declare class MCPClientManager {
133
134
  * @internal
134
135
  */
135
136
  private createAuthProvider;
137
+ /**
138
+ * Get saved RPC servers from storage (servers with rpc:// URLs).
139
+ * These are restored separately by the Agent class since they need env bindings.
140
+ */
141
+ getRpcServersFromStorage(): MCPServerRow[];
142
+ /**
143
+ * Save an RPC server to storage for hibernation recovery.
144
+ * The bindingName is stored in server_options so the Agent can look up
145
+ * the namespace from env during restore.
146
+ */
147
+ saveRpcServerToStorage(id: string, name: string, normalizedName: string, bindingName: string, props?: Record<string, unknown>): void;
136
148
  /**
137
149
  * Restore MCP server connections from storage
138
- * This method is called on Agent initialization to restore previously connected servers
150
+ * This method is called on Agent initialization to restore previously connected servers.
151
+ * RPC servers (rpc:// URLs) are skipped here -- they are restored by the Agent class
152
+ * which has access to env bindings.
139
153
  *
140
154
  * @param clientName Name to use for OAuth client (typically the agent instance name)
141
155
  */
142
156
  restoreConnectionsFromStorage(clientName: string): Promise<void>;
157
+ /**
158
+ * Track a pending connection promise for a server.
159
+ * The promise is removed from the map when it settles.
160
+ */
161
+ private _trackConnection;
162
+ /**
163
+ * Wait for all in-flight connection and discovery operations to settle.
164
+ * This is useful when you need MCP tools to be available before proceeding,
165
+ * e.g. before calling getAITools() after the agent wakes from hibernation.
166
+ *
167
+ * Returns once every pending connection has either connected and discovered,
168
+ * failed, or timed out. Never rejects.
169
+ *
170
+ * @param options.timeout - Maximum time in milliseconds to wait.
171
+ * `0` returns immediately without waiting.
172
+ * `undefined` (default) waits indefinitely.
173
+ */
174
+ waitForConnections(options?: {
175
+ timeout?: number;
176
+ }): Promise<void>;
143
177
  /**
144
178
  * Internal method to restore a single server connection and discovery
145
179
  */
@@ -218,11 +252,14 @@ declare class MCPClientManager {
218
252
  timeoutMs?: number;
219
253
  }): Promise<MCPDiscoverResult | undefined>;
220
254
  /**
221
- * Establish connection in the background after OAuth completion
222
- * This method connects to the server and discovers its capabilities
255
+ * Establish connection in the background after OAuth completion.
256
+ * This method connects to the server and discovers its capabilities.
257
+ * The connection is automatically tracked so that `waitForConnections()`
258
+ * will include it.
223
259
  * @param serverId The server ID to establish connection for
224
260
  */
225
261
  establishConnection(serverId: string): Promise<void>;
262
+ private _doEstablishConnection;
226
263
  /**
227
264
  * Configure OAuth callback handling
228
265
  * @param config OAuth callback configuration
@@ -333,7 +370,14 @@ declare class MCPClientManager {
333
370
  priority?: number | undefined;
334
371
  lastModified?: string | undefined;
335
372
  } | undefined;
336
- _meta?: Record<string, unknown> | undefined;
373
+ _meta
374
+ /**
375
+ * We need to delay loading ai sdk, because putting it in module scope is
376
+ * causing issues with startup time.
377
+ * The only place it's used is in getAITools, which only matters after
378
+ * .connect() is called on at least one server.
379
+ * So it's safe to delay loading it until .connect() is called.
380
+ */?: Record<string, unknown> | undefined;
337
381
  } | {
338
382
  type: "resource";
339
383
  resource: {
@@ -1,5 +1,5 @@
1
1
  import { tryN } from "../retries.js";
2
- import { a as Emitter, i as DisposableStore, n as MCPConnectionState, r as toErrorMessage, t as MCPClientConnection } from "../client-connection-CGMuV62J.js";
2
+ import { a as RPC_DO_PREFIX, c as Emitter, n as MCPConnectionState, o as toErrorMessage, s as DisposableStore, t as MCPClientConnection } from "../client-connection-D3Wcd6Q6.js";
3
3
  import { DurableObjectOAuthClientProvider } from "./do-oauth-client-provider.js";
4
4
  import { nanoid } from "nanoid";
5
5
  import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker-provider.js";
@@ -7,6 +7,47 @@ import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validatio
7
7
  //#region src/mcp/client.ts
8
8
  const defaultClientOptions = { jsonSchemaValidator: new CfWorkerJsonSchemaValidator() };
9
9
  /**
10
+ * Blocked hostname patterns for SSRF protection.
11
+ * Prevents MCP client from connecting to internal/private network addresses.
12
+ */
13
+ const BLOCKED_HOSTNAMES = new Set([
14
+ "localhost",
15
+ "0.0.0.0",
16
+ "[::1]",
17
+ "[::]",
18
+ "metadata.google.internal"
19
+ ]);
20
+ /**
21
+ * Check whether a hostname looks like a private/internal IP address.
22
+ * Blocks RFC 1918, link-local, loopback, and cloud metadata endpoints.
23
+ */
24
+ function isBlockedUrl(url) {
25
+ let parsed;
26
+ try {
27
+ parsed = new URL(url);
28
+ } catch {
29
+ return true;
30
+ }
31
+ const hostname = parsed.hostname;
32
+ if (BLOCKED_HOSTNAMES.has(hostname)) return true;
33
+ const ipv4Parts = hostname.split(".");
34
+ if (ipv4Parts.length === 4 && ipv4Parts.every((p) => /^\d{1,3}$/.test(p))) {
35
+ const [a, b] = ipv4Parts.map(Number);
36
+ if (a === 10) return true;
37
+ if (a === 172 && b >= 16 && b <= 31) return true;
38
+ if (a === 192 && b === 168) return true;
39
+ if (a === 127) return true;
40
+ if (a === 169 && b === 254) return true;
41
+ if (a === 0) return true;
42
+ }
43
+ if (hostname.startsWith("[") && hostname.endsWith("]")) {
44
+ const addr = hostname.slice(1, -1).toLowerCase();
45
+ if (addr.startsWith("fc") || addr.startsWith("fd")) return true;
46
+ if (addr.startsWith("fe80")) return true;
47
+ }
48
+ return false;
49
+ }
50
+ /**
10
51
  * Utility class that aggregates multiple MCP clients into one
11
52
  */
12
53
  var MCPClientManager = class {
@@ -22,6 +63,7 @@ var MCPClientManager = class {
22
63
  this._didWarnAboutUnstableGetAITools = false;
23
64
  this._connectionDisposables = /* @__PURE__ */ new Map();
24
65
  this._isRestored = false;
66
+ this._pendingConnections = /* @__PURE__ */ new Map();
25
67
  this._onObservabilityEvent = new Emitter();
26
68
  this.onObservabilityEvent = this._onObservabilityEvent.event;
27
69
  this._onServerStateChanged = new Emitter();
@@ -80,8 +122,36 @@ var MCPClientManager = class {
80
122
  return authProvider;
81
123
  }
82
124
  /**
125
+ * Get saved RPC servers from storage (servers with rpc:// URLs).
126
+ * These are restored separately by the Agent class since they need env bindings.
127
+ */
128
+ getRpcServersFromStorage() {
129
+ return this.getServersFromStorage().filter((s) => s.server_url.startsWith(RPC_DO_PREFIX));
130
+ }
131
+ /**
132
+ * Save an RPC server to storage for hibernation recovery.
133
+ * The bindingName is stored in server_options so the Agent can look up
134
+ * the namespace from env during restore.
135
+ */
136
+ saveRpcServerToStorage(id, name, normalizedName, bindingName, props) {
137
+ this.saveServerToStorage({
138
+ id,
139
+ name,
140
+ server_url: `${RPC_DO_PREFIX}${normalizedName}`,
141
+ client_id: null,
142
+ auth_url: null,
143
+ callback_url: "",
144
+ server_options: JSON.stringify({
145
+ bindingName,
146
+ props
147
+ })
148
+ });
149
+ }
150
+ /**
83
151
  * Restore MCP server connections from storage
84
- * This method is called on Agent initialization to restore previously connected servers
152
+ * This method is called on Agent initialization to restore previously connected servers.
153
+ * RPC servers (rpc:// URLs) are skipped here -- they are restored by the Agent class
154
+ * which has access to env bindings.
85
155
  *
86
156
  * @param clientName Name to use for OAuth client (typically the agent instance name)
87
157
  */
@@ -93,6 +163,7 @@ var MCPClientManager = class {
93
163
  return;
94
164
  }
95
165
  for (const server of servers) {
166
+ if (server.server_url.startsWith(RPC_DO_PREFIX)) continue;
96
167
  const existingConn = this.mcpConnections[server.id];
97
168
  if (existingConn) {
98
169
  if (existingConn.connectionState === MCPConnectionState.READY) {
@@ -112,9 +183,12 @@ var MCPClientManager = class {
112
183
  }
113
184
  }
114
185
  const parsedOptions = server.server_options ? JSON.parse(server.server_options) : null;
115
- const authProvider = this._createAuthProviderFn ? this._createAuthProviderFn(server.callback_url) : this.createAuthProvider(server.id, server.callback_url, clientName, server.client_id ?? void 0);
116
- authProvider.serverId = server.id;
117
- if (server.client_id) authProvider.clientId = server.client_id;
186
+ let authProvider;
187
+ if (server.callback_url) {
188
+ authProvider = this._createAuthProviderFn ? this._createAuthProviderFn(server.callback_url) : this.createAuthProvider(server.id, server.callback_url, clientName, server.client_id ?? void 0);
189
+ authProvider.serverId = server.id;
190
+ if (server.client_id) authProvider.clientId = server.client_id;
191
+ }
118
192
  const conn = this.createConnection(server.id, server.server_url, {
119
193
  client: parsedOptions?.client ?? {},
120
194
  transport: {
@@ -127,11 +201,46 @@ var MCPClientManager = class {
127
201
  conn.connectionState = MCPConnectionState.AUTHENTICATING;
128
202
  continue;
129
203
  }
130
- this._restoreServer(server.id, parsedOptions?.retry);
204
+ this._trackConnection(server.id, this._restoreServer(server.id, parsedOptions?.retry));
131
205
  }
132
206
  this._isRestored = true;
133
207
  }
134
208
  /**
209
+ * Track a pending connection promise for a server.
210
+ * The promise is removed from the map when it settles.
211
+ */
212
+ _trackConnection(serverId, promise) {
213
+ const tracked = promise.finally(() => {
214
+ if (this._pendingConnections.get(serverId) === tracked) this._pendingConnections.delete(serverId);
215
+ });
216
+ this._pendingConnections.set(serverId, tracked);
217
+ }
218
+ /**
219
+ * Wait for all in-flight connection and discovery operations to settle.
220
+ * This is useful when you need MCP tools to be available before proceeding,
221
+ * e.g. before calling getAITools() after the agent wakes from hibernation.
222
+ *
223
+ * Returns once every pending connection has either connected and discovered,
224
+ * failed, or timed out. Never rejects.
225
+ *
226
+ * @param options.timeout - Maximum time in milliseconds to wait.
227
+ * `0` returns immediately without waiting.
228
+ * `undefined` (default) waits indefinitely.
229
+ */
230
+ async waitForConnections(options) {
231
+ if (this._pendingConnections.size === 0) return;
232
+ if (options?.timeout != null && options.timeout <= 0) return;
233
+ const settled = Promise.allSettled(this._pendingConnections.values());
234
+ if (options?.timeout != null && options.timeout > 0) {
235
+ let timerId;
236
+ const timer = new Promise((resolve) => {
237
+ timerId = setTimeout(resolve, options.timeout);
238
+ });
239
+ await Promise.race([settled, timer]);
240
+ clearTimeout(timerId);
241
+ } else await settled;
242
+ }
243
+ /**
135
244
  * Internal method to restore a single server connection and discovery
136
245
  */
137
246
  async _restoreServer(serverId, retry) {
@@ -171,6 +280,7 @@ var MCPClientManager = class {
171
280
  options.transport.authProvider.serverId = id;
172
281
  if (options.reconnect?.oauthClientId) options.transport.authProvider.clientId = options.reconnect?.oauthClientId;
173
282
  }
283
+ if (isBlockedUrl(url)) throw new Error(`Blocked URL: ${url} — MCP client connections to private/internal addresses are not allowed`);
174
284
  if (!options.reconnect?.oauthCode || !this.mcpConnections[id]) {
175
285
  const normalizedTransport = {
176
286
  ...options.transport,
@@ -198,15 +308,13 @@ var MCPClientManager = class {
198
308
  } catch (error) {
199
309
  this._onObservabilityEvent.fire({
200
310
  type: "mcp:client:connect",
201
- displayMessage: `Failed to complete OAuth reconnection for ${id} for ${url}`,
202
311
  payload: {
203
312
  url,
204
313
  transport: options.transport?.type ?? "auto",
205
314
  state: this.mcpConnections[id].connectionState,
206
315
  error: toErrorMessage(error)
207
316
  },
208
- timestamp: Date.now(),
209
- id
317
+ timestamp: Date.now()
210
318
  });
211
319
  throw error;
212
320
  }
@@ -259,6 +367,7 @@ var MCPClientManager = class {
259
367
  * @returns Server ID
260
368
  */
261
369
  async registerServer(id, options) {
370
+ if (isBlockedUrl(options.url)) throw new Error(`Blocked URL: ${options.url} — MCP client connections to private/internal addresses are not allowed`);
262
371
  this.createConnection(id, options.url, {
263
372
  client: options.client,
264
373
  transport: {
@@ -271,7 +380,7 @@ var MCPClientManager = class {
271
380
  id,
272
381
  name: options.name,
273
382
  server_url: options.url,
274
- callback_url: options.callbackUrl,
383
+ callback_url: options.callbackUrl ?? "",
275
384
  client_id: options.clientId ?? null,
276
385
  auth_url: options.authUrl ?? null,
277
386
  server_options: JSON.stringify({
@@ -324,6 +433,15 @@ var MCPClientManager = class {
324
433
  });
325
434
  this._onServerStateChanged.fire();
326
435
  }
436
+ this._onObservabilityEvent.fire({
437
+ type: "mcp:client:authorize",
438
+ payload: {
439
+ serverId: id,
440
+ authUrl,
441
+ clientId
442
+ },
443
+ timestamp: Date.now()
444
+ });
327
445
  return {
328
446
  state: conn.connectionState,
329
447
  authUrl,
@@ -459,10 +577,8 @@ var MCPClientManager = class {
459
577
  if (!conn) {
460
578
  this._onObservabilityEvent.fire({
461
579
  type: "mcp:client:discover",
462
- displayMessage: `Connection not found for ${serverId}`,
463
580
  payload: {},
464
- timestamp: Date.now(),
465
- id: nanoid()
581
+ timestamp: Date.now()
466
582
  });
467
583
  return;
468
584
  }
@@ -474,33 +590,36 @@ var MCPClientManager = class {
474
590
  };
475
591
  }
476
592
  /**
477
- * Establish connection in the background after OAuth completion
478
- * This method connects to the server and discovers its capabilities
593
+ * Establish connection in the background after OAuth completion.
594
+ * This method connects to the server and discovers its capabilities.
595
+ * The connection is automatically tracked so that `waitForConnections()`
596
+ * will include it.
479
597
  * @param serverId The server ID to establish connection for
480
598
  */
481
599
  async establishConnection(serverId) {
600
+ const promise = this._doEstablishConnection(serverId);
601
+ this._trackConnection(serverId, promise);
602
+ return promise;
603
+ }
604
+ async _doEstablishConnection(serverId) {
482
605
  const conn = this.mcpConnections[serverId];
483
606
  if (!conn) {
484
607
  this._onObservabilityEvent.fire({
485
608
  type: "mcp:client:preconnect",
486
- displayMessage: `Connection not found for serverId: ${serverId}`,
487
609
  payload: { serverId },
488
- timestamp: Date.now(),
489
- id: nanoid()
610
+ timestamp: Date.now()
490
611
  });
491
612
  return;
492
613
  }
493
614
  if (conn.connectionState === MCPConnectionState.DISCOVERING || conn.connectionState === MCPConnectionState.READY) {
494
615
  this._onObservabilityEvent.fire({
495
616
  type: "mcp:client:connect",
496
- displayMessage: `establishConnection skipped for ${serverId}, already in ${conn.connectionState} state`,
497
617
  payload: {
498
618
  url: conn.url.toString(),
499
619
  transport: conn.options.transport.type || "unknown",
500
620
  state: conn.connectionState
501
621
  },
502
- timestamp: Date.now(),
503
- id: nanoid()
622
+ timestamp: Date.now()
504
623
  });
505
624
  return;
506
625
  }
@@ -513,14 +632,12 @@ var MCPClientManager = class {
513
632
  if (connectResult.state === MCPConnectionState.CONNECTED) await this.discoverIfConnected(serverId);
514
633
  this._onObservabilityEvent.fire({
515
634
  type: "mcp:client:connect",
516
- displayMessage: `establishConnection completed for ${serverId}, final state: ${conn.connectionState}`,
517
635
  payload: {
518
636
  url: conn.url.toString(),
519
637
  transport: conn.options.transport.type || "unknown",
520
638
  state: conn.connectionState
521
639
  },
522
- timestamp: Date.now(),
523
- id: nanoid()
640
+ timestamp: Date.now()
524
641
  });
525
642
  }
526
643
  /**
@@ -566,8 +683,10 @@ var MCPClientManager = class {
566
683
  getAITools() {
567
684
  if (!this.jsonSchema) throw new Error("jsonSchema not initialized.");
568
685
  for (const [id, conn] of Object.entries(this.mcpConnections)) if (conn.connectionState !== MCPConnectionState.READY && conn.connectionState !== MCPConnectionState.AUTHENTICATING) console.warn(`[getAITools] WARNING: Reading tools from connection ${id} in state "${conn.connectionState}". Tools may not be loaded yet.`);
569
- return Object.fromEntries(getNamespacedData(this.mcpConnections, "tools").map((tool) => {
570
- return [`tool_${tool.serverId.replace(/-/g, "")}_${tool.name}`, {
686
+ const entries = [];
687
+ for (const tool of getNamespacedData(this.mcpConnections, "tools")) try {
688
+ const toolKey = `tool_${tool.serverId.replace(/-/g, "")}_${tool.name}`;
689
+ entries.push([toolKey, {
571
690
  description: tool.description,
572
691
  execute: async (args) => {
573
692
  const result = await this.callTool({
@@ -582,10 +701,13 @@ var MCPClientManager = class {
582
701
  }
583
702
  return result;
584
703
  },
585
- inputSchema: this.jsonSchema(tool.inputSchema),
704
+ inputSchema: tool.inputSchema ? this.jsonSchema(tool.inputSchema) : this.jsonSchema({ type: "object" }),
586
705
  outputSchema: tool.outputSchema ? this.jsonSchema(tool.outputSchema) : void 0
587
- }];
588
- }));
706
+ }]);
707
+ } catch (e) {
708
+ console.warn(`[getAITools] Skipping tool "${tool.name}" from "${tool.serverId}": ${e}`);
709
+ }
710
+ return Object.fromEntries(entries);
589
711
  }
590
712
  /**
591
713
  * @deprecated this has been renamed to getAITools(), and unstable_getAITools will be removed in the next major version
@@ -610,6 +732,7 @@ var MCPClientManager = class {
610
732
  */
611
733
  async closeAllConnections() {
612
734
  const ids = Object.keys(this.mcpConnections);
735
+ this._pendingConnections.clear();
613
736
  for (const id of ids) this.mcpConnections[id].cancelDiscovery();
614
737
  await Promise.all(ids.map(async (id) => {
615
738
  await this.mcpConnections[id].client.close();
@@ -628,6 +751,7 @@ var MCPClientManager = class {
628
751
  async closeConnection(id) {
629
752
  if (!this.mcpConnections[id]) throw new Error(`Connection with id "${id}" does not exist.`);
630
753
  this.mcpConnections[id].cancelDiscovery();
754
+ this._pendingConnections.delete(id);
631
755
  await this.mcpConnections[id].client.close();
632
756
  delete this.mcpConnections[id];
633
757
  const store = this._connectionDisposables.get(id);
@@ -683,9 +807,10 @@ var MCPClientManager = class {
683
807
  * Namespaced version of callTool
684
808
  */
685
809
  async callTool(params, resultSchema, options) {
686
- const unqualifiedName = params.name.replace(`${params.serverId}.`, "");
687
- return this.mcpConnections[params.serverId].client.callTool({
688
- ...params,
810
+ const { serverId, ...mcpParams } = params;
811
+ const unqualifiedName = mcpParams.name.replace(`${serverId}.`, "");
812
+ return this.mcpConnections[serverId].client.callTool({
813
+ ...mcpParams,
689
814
  name: unqualifiedName
690
815
  }, resultSchema, options);
691
816
  }