agents 0.0.0-49fb428 → 0.0.0-4abd78a

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.
Files changed (56) hide show
  1. package/README.md +255 -27
  2. package/dist/ai-chat-agent.d.ts +57 -7
  3. package/dist/ai-chat-agent.js +284 -93
  4. package/dist/ai-chat-agent.js.map +1 -1
  5. package/dist/ai-chat-v5-migration.d.ts +152 -0
  6. package/dist/ai-chat-v5-migration.js +19 -0
  7. package/dist/ai-react.d.ts +77 -67
  8. package/dist/ai-react.js +213 -66
  9. package/dist/ai-react.js.map +1 -1
  10. package/dist/ai-types.d.ts +40 -18
  11. package/dist/ai-types.js +6 -0
  12. package/dist/chunk-AVYJQSLW.js +17 -0
  13. package/dist/chunk-AVYJQSLW.js.map +1 -0
  14. package/dist/chunk-LL2AFX7V.js +109 -0
  15. package/dist/chunk-LL2AFX7V.js.map +1 -0
  16. package/dist/chunk-M5SZRJTA.js +1300 -0
  17. package/dist/chunk-M5SZRJTA.js.map +1 -0
  18. package/dist/chunk-MH46VMM4.js +612 -0
  19. package/dist/chunk-MH46VMM4.js.map +1 -0
  20. package/dist/chunk-QEVM4BVL.js +116 -0
  21. package/dist/chunk-QEVM4BVL.js.map +1 -0
  22. package/dist/chunk-UJVEAURM.js +150 -0
  23. package/dist/chunk-UJVEAURM.js.map +1 -0
  24. package/dist/client-CvaJdLQA.d.ts +5015 -0
  25. package/dist/client.d.ts +16 -2
  26. package/dist/client.js +7 -126
  27. package/dist/client.js.map +1 -1
  28. package/dist/index.d.ts +284 -22
  29. package/dist/index.js +17 -6
  30. package/dist/mcp/client.d.ts +9 -761
  31. package/dist/mcp/client.js +3 -402
  32. package/dist/mcp/client.js.map +1 -1
  33. package/dist/mcp/do-oauth-client-provider.d.ts +4 -3
  34. package/dist/mcp/do-oauth-client-provider.js +3 -103
  35. package/dist/mcp/do-oauth-client-provider.js.map +1 -1
  36. package/dist/mcp/index.d.ts +83 -31
  37. package/dist/mcp/index.js +947 -267
  38. package/dist/mcp/index.js.map +1 -1
  39. package/dist/observability/index.d.ts +46 -0
  40. package/dist/observability/index.js +11 -0
  41. package/dist/observability/index.js.map +1 -0
  42. package/dist/react.d.ts +89 -5
  43. package/dist/react.js +23 -9
  44. package/dist/react.js.map +1 -1
  45. package/dist/schedule.d.ts +81 -7
  46. package/dist/schedule.js +19 -8
  47. package/dist/schedule.js.map +1 -1
  48. package/dist/serializable.d.ts +32 -0
  49. package/dist/serializable.js +1 -0
  50. package/dist/serializable.js.map +1 -0
  51. package/package.json +87 -52
  52. package/src/index.ts +1193 -152
  53. package/dist/chunk-HMLY7DHA.js +0 -16
  54. package/dist/chunk-XG52S6YY.js +0 -591
  55. package/dist/chunk-XG52S6YY.js.map +0 -1
  56. /package/dist/{chunk-HMLY7DHA.js.map → ai-chat-v5-migration.js.map} +0 -0
package/src/index.ts CHANGED
@@ -1,19 +1,34 @@
1
+ import type { env } from "cloudflare:workers";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
+ import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
5
+
6
+ import type {
7
+ Prompt,
8
+ Resource,
9
+ ServerCapabilities,
10
+ Tool
11
+ } from "@modelcontextprotocol/sdk/types.js";
12
+ import { parseCronExpression } from "cron-schedule";
13
+ import { nanoid } from "nanoid";
14
+ import { EmailMessage } from "cloudflare:email";
1
15
  import {
2
- Server,
3
- routePartykitRequest,
4
- type PartyServerOptions,
5
- getServerByName,
6
16
  type Connection,
7
17
  type ConnectionContext,
18
+ type PartyServerOptions,
19
+ Server,
8
20
  type WSMessage,
21
+ getServerByName,
22
+ routePartykitRequest
9
23
  } from "partyserver";
24
+ import { camelCaseToKebabCase } from "./client";
25
+ import { MCPClientManager } from "./mcp/client";
26
+ // import type { MCPClientConnection } from "./mcp/client-connection";
27
+ import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider";
28
+ import { genericObservability, type Observability } from "./observability";
29
+ import { MessageType } from "./ai-types";
10
30
 
11
- import { parseCronExpression } from "cron-schedule";
12
- import { nanoid } from "nanoid";
13
-
14
- import { AsyncLocalStorage } from "node:async_hooks";
15
-
16
- export type { Connection, WSMessage, ConnectionContext } from "partyserver";
31
+ export type { Connection, ConnectionContext, WSMessage } from "partyserver";
17
32
 
18
33
  /**
19
34
  * RPC request message from client
@@ -29,7 +44,7 @@ export type RPCRequest = {
29
44
  * State update message from client
30
45
  */
31
46
  export type StateUpdateMessage = {
32
- type: "cf_agent_state";
47
+ type: MessageType.CF_AGENT_STATE;
33
48
  state: unknown;
34
49
  };
35
50
 
@@ -37,7 +52,7 @@ export type StateUpdateMessage = {
37
52
  * RPC response message to client
38
53
  */
39
54
  export type RPCResponse = {
40
- type: "rpc";
55
+ type: MessageType.RPC;
41
56
  id: string;
42
57
  } & (
43
58
  | {
@@ -64,7 +79,7 @@ function isRPCRequest(msg: unknown): msg is RPCRequest {
64
79
  typeof msg === "object" &&
65
80
  msg !== null &&
66
81
  "type" in msg &&
67
- msg.type === "rpc" &&
82
+ msg.type === MessageType.RPC &&
68
83
  "id" in msg &&
69
84
  typeof msg.id === "string" &&
70
85
  "method" in msg &&
@@ -82,7 +97,7 @@ function isStateUpdateMessage(msg: unknown): msg is StateUpdateMessage {
82
97
  typeof msg === "object" &&
83
98
  msg !== null &&
84
99
  "type" in msg &&
85
- msg.type === "cf_agent_state" &&
100
+ msg.type === MessageType.CF_AGENT_STATE &&
86
101
  "state" in msg
87
102
  );
88
103
  }
@@ -97,16 +112,16 @@ export type CallableMetadata = {
97
112
  streaming?: boolean;
98
113
  };
99
114
 
100
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
101
115
  const callableMetadata = new Map<Function, CallableMetadata>();
102
116
 
103
117
  /**
104
118
  * Decorator that marks a method as callable by clients
105
119
  * @param metadata Optional metadata about the callable method
106
120
  */
107
- export function unstable_callable(metadata: CallableMetadata = {}) {
121
+ export function callable(metadata: CallableMetadata = {}) {
108
122
  return function callableDecorator<This, Args extends unknown[], Return>(
109
123
  target: (this: This, ...args: Args) => Return,
124
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: later
110
125
  context: ClassMethodDecoratorContext
111
126
  ) {
112
127
  if (!callableMetadata.has(target)) {
@@ -117,6 +132,30 @@ export function unstable_callable(metadata: CallableMetadata = {}) {
117
132
  };
118
133
  }
119
134
 
135
+ let didWarnAboutUnstableCallable = false;
136
+
137
+ /**
138
+ * Decorator that marks a method as callable by clients
139
+ * @deprecated this has been renamed to callable, and unstable_callable will be removed in the next major version
140
+ * @param metadata Optional metadata about the callable method
141
+ */
142
+ export const unstable_callable = (metadata: CallableMetadata = {}) => {
143
+ if (!didWarnAboutUnstableCallable) {
144
+ didWarnAboutUnstableCallable = true;
145
+ console.warn(
146
+ "unstable_callable is deprecated, use callable instead. unstable_callable will be removed in the next major version."
147
+ );
148
+ }
149
+ callable(metadata);
150
+ };
151
+
152
+ export type QueueItem<T = string> = {
153
+ id: string;
154
+ payload: T;
155
+ callback: keyof Agent<unknown>;
156
+ created_at: number;
157
+ };
158
+
120
159
  /**
121
160
  * Represents a scheduled task within an Agent
122
161
  * @template T Type of the payload data
@@ -158,24 +197,128 @@ function getNextCronTime(cron: string) {
158
197
  return interval.getNextDate();
159
198
  }
160
199
 
200
+ /**
201
+ * MCP Server state update message from server -> Client
202
+ */
203
+ export type MCPServerMessage = {
204
+ type: MessageType.CF_AGENT_MCP_SERVERS;
205
+ mcp: MCPServersState;
206
+ };
207
+
208
+ export type MCPServersState = {
209
+ servers: {
210
+ [id: string]: MCPServer;
211
+ };
212
+ tools: Tool[];
213
+ prompts: Prompt[];
214
+ resources: Resource[];
215
+ };
216
+
217
+ export type MCPServer = {
218
+ name: string;
219
+ server_url: string;
220
+ auth_url: string | null;
221
+ // This state is specifically about the temporary process of getting a token (if needed).
222
+ // Scope outside of that can't be relied upon because when the DO sleeps, there's no way
223
+ // to communicate a change to a non-ready state.
224
+ state: "authenticating" | "connecting" | "ready" | "discovering" | "failed";
225
+ instructions: string | null;
226
+ capabilities: ServerCapabilities | null;
227
+ };
228
+
229
+ /**
230
+ * MCP Server data stored in DO SQL for resuming MCP Server connections
231
+ */
232
+ type MCPServerRow = {
233
+ id: string;
234
+ name: string;
235
+ server_url: string;
236
+ client_id: string | null;
237
+ auth_url: string | null;
238
+ callback_url: string;
239
+ server_options: string;
240
+ };
241
+
161
242
  const STATE_ROW_ID = "cf_state_row_id";
162
243
  const STATE_WAS_CHANGED = "cf_state_was_changed";
163
244
 
164
245
  const DEFAULT_STATE = {} as unknown;
165
246
 
166
- export const unstable_context = new AsyncLocalStorage<{
167
- agent: Agent<unknown>;
247
+ const agentContext = new AsyncLocalStorage<{
248
+ agent: Agent<unknown, unknown>;
168
249
  connection: Connection | undefined;
169
250
  request: Request | undefined;
251
+ email: AgentEmail | undefined;
170
252
  }>();
171
253
 
254
+ export function getCurrentAgent<
255
+ T extends Agent<unknown, unknown> = Agent<unknown, unknown>
256
+ >(): {
257
+ agent: T | undefined;
258
+ connection: Connection | undefined;
259
+ request: Request | undefined;
260
+ email: AgentEmail | undefined;
261
+ } {
262
+ const store = agentContext.getStore() as
263
+ | {
264
+ agent: T;
265
+ connection: Connection | undefined;
266
+ request: Request | undefined;
267
+ email: AgentEmail | undefined;
268
+ }
269
+ | undefined;
270
+ if (!store) {
271
+ return {
272
+ agent: undefined,
273
+ connection: undefined,
274
+ request: undefined,
275
+ email: undefined
276
+ };
277
+ }
278
+ return store;
279
+ }
280
+
281
+ /**
282
+ * Wraps a method to run within the agent context, ensuring getCurrentAgent() works properly
283
+ * @param agent The agent instance
284
+ * @param method The method to wrap
285
+ * @returns A wrapped method that runs within the agent context
286
+ */
287
+
288
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
289
+ function withAgentContext<T extends (...args: any[]) => any>(
290
+ method: T
291
+ ): (this: Agent<unknown, unknown>, ...args: Parameters<T>) => ReturnType<T> {
292
+ return function (...args: Parameters<T>): ReturnType<T> {
293
+ const { connection, request, email, agent } = getCurrentAgent();
294
+
295
+ if (agent === this) {
296
+ // already wrapped, so we can just call the method
297
+ return method.apply(this, args);
298
+ }
299
+ // not wrapped, so we need to wrap it
300
+ return agentContext.run({ agent: this, connection, request, email }, () => {
301
+ return method.apply(this, args);
302
+ });
303
+ };
304
+ }
305
+
172
306
  /**
173
307
  * Base class for creating Agent implementations
174
308
  * @template Env Environment type containing bindings
175
309
  * @template State State type to store within the Agent
176
310
  */
177
- export class Agent<Env, State = unknown> extends Server<Env> {
178
- #state = DEFAULT_STATE as State;
311
+ export class Agent<
312
+ Env = typeof env,
313
+ State = unknown,
314
+ Props extends Record<string, unknown> = Record<string, unknown>
315
+ > extends Server<Env, Props> {
316
+ private _state = DEFAULT_STATE as State;
317
+
318
+ private _ParentClass: typeof Agent<Env, State> =
319
+ Object.getPrototypeOf(this).constructor;
320
+
321
+ mcp: MCPClientManager = new MCPClientManager(this._ParentClass.name, "0.0.1");
179
322
 
180
323
  /**
181
324
  * Initial state for the Agent
@@ -187,9 +330,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
187
330
  * Current state of the Agent
188
331
  */
189
332
  get state(): State {
190
- if (this.#state !== DEFAULT_STATE) {
333
+ if (this._state !== DEFAULT_STATE) {
191
334
  // state was previously set, and populated internal state
192
- return this.#state;
335
+ return this._state;
193
336
  }
194
337
  // looks like this is the first time the state is being accessed
195
338
  // check if the state was set in a previous life
@@ -209,8 +352,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
209
352
  ) {
210
353
  const state = result[0]?.state as string; // could be null?
211
354
 
212
- this.#state = JSON.parse(state);
213
- return this.#state;
355
+ this._state = JSON.parse(state);
356
+ return this._state;
214
357
  }
215
358
 
216
359
  // ok, this is the first time the state is being accessed
@@ -231,9 +374,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
231
374
  */
232
375
  static options = {
233
376
  /** Whether the Agent should hibernate when inactive */
234
- hibernate: true, // default to hibernate
377
+ hibernate: true // default to hibernate
235
378
  };
236
379
 
380
+ /**
381
+ * The observability implementation to use for the Agent
382
+ */
383
+ observability?: Observability = genericObservability;
384
+
237
385
  /**
238
386
  * Execute SQL queries against the Agent's database
239
387
  * @template T Type of the returned rows
@@ -263,6 +411,12 @@ export class Agent<Env, State = unknown> extends Server<Env> {
263
411
  constructor(ctx: AgentContext, env: Env) {
264
412
  super(ctx, env);
265
413
 
414
+ if (!wrappedClasses.has(this.constructor)) {
415
+ // Auto-wrap custom methods with agent context
416
+ this._autoWrapCustomMethods();
417
+ wrappedClasses.add(this.constructor);
418
+ }
419
+
266
420
  this.sql`
267
421
  CREATE TABLE IF NOT EXISTS cf_agents_state (
268
422
  id TEXT PRIMARY KEY NOT NULL,
@@ -270,8 +424,17 @@ export class Agent<Env, State = unknown> extends Server<Env> {
270
424
  )
271
425
  `;
272
426
 
427
+ this.sql`
428
+ CREATE TABLE IF NOT EXISTS cf_agents_queues (
429
+ id TEXT PRIMARY KEY NOT NULL,
430
+ payload TEXT,
431
+ callback TEXT,
432
+ created_at INTEGER DEFAULT (unixepoch())
433
+ )
434
+ `;
435
+
273
436
  void this.ctx.blockConcurrencyWhile(async () => {
274
- return this.#tryCatch(async () => {
437
+ return this._tryCatch(async () => {
275
438
  // Create alarms table if it doesn't exist
276
439
  this.sql`
277
440
  CREATE TABLE IF NOT EXISTS cf_agents_schedules (
@@ -291,25 +454,65 @@ export class Agent<Env, State = unknown> extends Server<Env> {
291
454
  });
292
455
  });
293
456
 
457
+ this.sql`
458
+ CREATE TABLE IF NOT EXISTS cf_agents_mcp_servers (
459
+ id TEXT PRIMARY KEY NOT NULL,
460
+ name TEXT NOT NULL,
461
+ server_url TEXT NOT NULL,
462
+ callback_url TEXT NOT NULL,
463
+ client_id TEXT,
464
+ auth_url TEXT,
465
+ server_options TEXT
466
+ )
467
+ `;
468
+
469
+ const _onRequest = this.onRequest.bind(this);
470
+ this.onRequest = (request: Request) => {
471
+ return agentContext.run(
472
+ { agent: this, connection: undefined, request, email: undefined },
473
+ async () => {
474
+ if (this.mcp.isCallbackRequest(request)) {
475
+ await this.mcp.handleCallbackRequest(request);
476
+
477
+ // after the MCP connection handshake, we can send updated mcp state
478
+ this.broadcast(
479
+ JSON.stringify({
480
+ mcp: this.getMcpServers(),
481
+ type: MessageType.CF_AGENT_MCP_SERVERS
482
+ })
483
+ );
484
+
485
+ // We probably should let the user configure this response/redirect, but this is fine for now.
486
+ return new Response("<script>window.close();</script>", {
487
+ headers: { "content-type": "text/html" },
488
+ status: 200
489
+ });
490
+ }
491
+
492
+ return this._tryCatch(() => _onRequest(request));
493
+ }
494
+ );
495
+ };
496
+
294
497
  const _onMessage = this.onMessage.bind(this);
295
498
  this.onMessage = async (connection: Connection, message: WSMessage) => {
296
- return unstable_context.run(
297
- { agent: this, connection, request: undefined },
499
+ return agentContext.run(
500
+ { agent: this, connection, request: undefined, email: undefined },
298
501
  async () => {
299
502
  if (typeof message !== "string") {
300
- return this.#tryCatch(() => _onMessage(connection, message));
503
+ return this._tryCatch(() => _onMessage(connection, message));
301
504
  }
302
505
 
303
506
  let parsed: unknown;
304
507
  try {
305
508
  parsed = JSON.parse(message);
306
- } catch (e) {
509
+ } catch (_e) {
307
510
  // silently fail and let the onMessage handler handle it
308
- return this.#tryCatch(() => _onMessage(connection, message));
511
+ return this._tryCatch(() => _onMessage(connection, message));
309
512
  }
310
513
 
311
514
  if (isStateUpdateMessage(parsed)) {
312
- this.#setStateInternal(parsed.state as State, connection);
515
+ this._setStateInternal(parsed.state as State, connection);
313
516
  return;
314
517
  }
315
518
 
@@ -323,11 +526,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
323
526
  throw new Error(`Method ${method} does not exist`);
324
527
  }
325
528
 
326
- if (!this.#isCallable(method)) {
529
+ if (!this._isCallable(method)) {
327
530
  throw new Error(`Method ${method} is not callable`);
328
531
  }
329
532
 
330
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
331
533
  const metadata = callableMetadata.get(methodFn as Function);
332
534
 
333
535
  // For streaming methods, pass a StreamingResponse object
@@ -339,22 +541,37 @@ export class Agent<Env, State = unknown> extends Server<Env> {
339
541
 
340
542
  // For regular methods, execute and send response
341
543
  const result = await methodFn.apply(this, args);
544
+
545
+ this.observability?.emit(
546
+ {
547
+ displayMessage: `RPC call to ${method}`,
548
+ id: nanoid(),
549
+ payload: {
550
+ method,
551
+ streaming: metadata?.streaming
552
+ },
553
+ timestamp: Date.now(),
554
+ type: "rpc"
555
+ },
556
+ this.ctx
557
+ );
558
+
342
559
  const response: RPCResponse = {
343
- type: "rpc",
560
+ done: true,
344
561
  id,
345
- success: true,
346
562
  result,
347
- done: true,
563
+ success: true,
564
+ type: MessageType.RPC
348
565
  };
349
566
  connection.send(JSON.stringify(response));
350
567
  } catch (e) {
351
568
  // Send error response
352
569
  const response: RPCResponse = {
353
- type: "rpc",
354
- id: parsed.id,
355
- success: false,
356
570
  error:
357
571
  e instanceof Error ? e.message : "Unknown error occurred",
572
+ id: parsed.id,
573
+ success: false,
574
+ type: MessageType.RPC
358
575
  };
359
576
  connection.send(JSON.stringify(response));
360
577
  console.error("RPC error:", e);
@@ -362,7 +579,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
362
579
  return;
363
580
  }
364
581
 
365
- return this.#tryCatch(() => _onMessage(connection, message));
582
+ return this._tryCatch(() => _onMessage(connection, message));
366
583
  }
367
584
  );
368
585
  };
@@ -371,27 +588,115 @@ export class Agent<Env, State = unknown> extends Server<Env> {
371
588
  this.onConnect = (connection: Connection, ctx: ConnectionContext) => {
372
589
  // TODO: This is a hack to ensure the state is sent after the connection is established
373
590
  // must fix this
374
- return unstable_context.run(
375
- { agent: this, connection, request: ctx.request },
591
+ return agentContext.run(
592
+ { agent: this, connection, request: ctx.request, email: undefined },
593
+ () => {
594
+ if (this.state) {
595
+ connection.send(
596
+ JSON.stringify({
597
+ state: this.state,
598
+ type: MessageType.CF_AGENT_STATE
599
+ })
600
+ );
601
+ }
602
+
603
+ connection.send(
604
+ JSON.stringify({
605
+ mcp: this.getMcpServers(),
606
+ type: MessageType.CF_AGENT_MCP_SERVERS
607
+ })
608
+ );
609
+
610
+ this.observability?.emit(
611
+ {
612
+ displayMessage: "Connection established",
613
+ id: nanoid(),
614
+ payload: {
615
+ connectionId: connection.id
616
+ },
617
+ timestamp: Date.now(),
618
+ type: "connect"
619
+ },
620
+ this.ctx
621
+ );
622
+ return this._tryCatch(() => _onConnect(connection, ctx));
623
+ }
624
+ );
625
+ };
626
+
627
+ const _onStart = this.onStart.bind(this);
628
+ this.onStart = async (props?: Props) => {
629
+ return agentContext.run(
630
+ {
631
+ agent: this,
632
+ connection: undefined,
633
+ request: undefined,
634
+ email: undefined
635
+ },
376
636
  async () => {
377
- setTimeout(() => {
378
- if (this.state) {
379
- connection.send(
380
- JSON.stringify({
381
- type: "cf_agent_state",
382
- state: this.state,
383
- })
384
- );
637
+ await this._tryCatch(() => {
638
+ const servers = this.sql<MCPServerRow>`
639
+ SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
640
+ `;
641
+
642
+ this.broadcast(
643
+ JSON.stringify({
644
+ mcp: this.getMcpServers(),
645
+ type: MessageType.CF_AGENT_MCP_SERVERS
646
+ })
647
+ );
648
+
649
+ // from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
650
+ if (servers && Array.isArray(servers) && servers.length > 0) {
651
+ servers.forEach((server) => {
652
+ this._connectToMcpServerInternal(
653
+ server.name,
654
+ server.server_url,
655
+ server.callback_url,
656
+ server.server_options
657
+ ? JSON.parse(server.server_options)
658
+ : undefined,
659
+ {
660
+ id: server.id,
661
+ oauthClientId: server.client_id ?? undefined
662
+ }
663
+ )
664
+ .then(() => {
665
+ // Broadcast updated MCP servers state after each server connects
666
+ this.broadcast(
667
+ JSON.stringify({
668
+ mcp: this.getMcpServers(),
669
+ type: MessageType.CF_AGENT_MCP_SERVERS
670
+ })
671
+ );
672
+ })
673
+ .catch((error) => {
674
+ console.error(
675
+ `Error connecting to MCP server: ${server.name} (${server.server_url})`,
676
+ error
677
+ );
678
+ // Still broadcast even if connection fails, so clients know about the failure
679
+ this.broadcast(
680
+ JSON.stringify({
681
+ mcp: this.getMcpServers(),
682
+ type: MessageType.CF_AGENT_MCP_SERVERS
683
+ })
684
+ );
685
+ });
686
+ });
385
687
  }
386
- return this.#tryCatch(() => _onConnect(connection, ctx));
387
- }, 20);
688
+ return _onStart(props);
689
+ });
388
690
  }
389
691
  );
390
692
  };
391
693
  }
392
694
 
393
- #setStateInternal(state: State, source: Connection | "server" = "server") {
394
- this.#state = state;
695
+ private _setStateInternal(
696
+ state: State,
697
+ source: Connection | "server" = "server"
698
+ ) {
699
+ this._state = state;
395
700
  this.sql`
396
701
  INSERT OR REPLACE INTO cf_agents_state (id, state)
397
702
  VALUES (${STATE_ROW_ID}, ${JSON.stringify(state)})
@@ -402,16 +707,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
402
707
  `;
403
708
  this.broadcast(
404
709
  JSON.stringify({
405
- type: "cf_agent_state",
406
710
  state: state,
711
+ type: MessageType.CF_AGENT_STATE
407
712
  }),
408
713
  source !== "server" ? [source.id] : []
409
714
  );
410
- return this.#tryCatch(() => {
411
- const { connection, request } = unstable_context.getStore() || {};
412
- return unstable_context.run(
413
- { agent: this, connection, request },
715
+ return this._tryCatch(() => {
716
+ const { connection, request, email } = agentContext.getStore() || {};
717
+ return agentContext.run(
718
+ { agent: this, connection, request, email },
414
719
  async () => {
720
+ this.observability?.emit(
721
+ {
722
+ displayMessage: "State updated",
723
+ id: nanoid(),
724
+ payload: {},
725
+ timestamp: Date.now(),
726
+ type: "state:update"
727
+ },
728
+ this.ctx
729
+ );
415
730
  return this.onStateUpdate(state, source);
416
731
  }
417
732
  );
@@ -423,7 +738,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
423
738
  * @param state New state to set
424
739
  */
425
740
  setState(state: State) {
426
- this.#setStateInternal(state, "server");
741
+ this._setStateInternal(state, "server");
427
742
  }
428
743
 
429
744
  /**
@@ -431,24 +746,90 @@ export class Agent<Env, State = unknown> extends Server<Env> {
431
746
  * @param state Updated state
432
747
  * @param source Source of the state update ("server" or a client connection)
433
748
  */
749
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
434
750
  onStateUpdate(state: State | undefined, source: Connection | "server") {
435
751
  // override this to handle state updates
436
752
  }
437
753
 
438
754
  /**
439
- * Called when the Agent receives an email
755
+ * Called when the Agent receives an email via routeAgentEmail()
756
+ * Override this method to handle incoming emails
440
757
  * @param email Email message to process
441
758
  */
442
- onEmail(email: ForwardableEmailMessage) {
443
- return unstable_context.run(
444
- { agent: this, connection: undefined, request: undefined },
759
+ async _onEmail(email: AgentEmail) {
760
+ // nb: we use this roundabout way of getting to onEmail
761
+ // because of https://github.com/cloudflare/workerd/issues/4499
762
+ return agentContext.run(
763
+ { agent: this, connection: undefined, request: undefined, email: email },
445
764
  async () => {
446
- console.error("onEmail not implemented");
765
+ if ("onEmail" in this && typeof this.onEmail === "function") {
766
+ return this._tryCatch(() =>
767
+ (this.onEmail as (email: AgentEmail) => Promise<void>)(email)
768
+ );
769
+ } else {
770
+ console.log("Received email from:", email.from, "to:", email.to);
771
+ console.log("Subject:", email.headers.get("subject"));
772
+ console.log(
773
+ "Implement onEmail(email: AgentEmail): Promise<void> in your agent to process emails"
774
+ );
775
+ }
447
776
  }
448
777
  );
449
778
  }
450
779
 
451
- async #tryCatch<T>(fn: () => T | Promise<T>) {
780
+ /**
781
+ * Reply to an email
782
+ * @param email The email to reply to
783
+ * @param options Options for the reply
784
+ * @returns void
785
+ */
786
+ async replyToEmail(
787
+ email: AgentEmail,
788
+ options: {
789
+ fromName: string;
790
+ subject?: string | undefined;
791
+ body: string;
792
+ contentType?: string;
793
+ headers?: Record<string, string>;
794
+ }
795
+ ): Promise<void> {
796
+ return this._tryCatch(async () => {
797
+ const agentName = camelCaseToKebabCase(this._ParentClass.name);
798
+ const agentId = this.name;
799
+
800
+ const { createMimeMessage } = await import("mimetext");
801
+ const msg = createMimeMessage();
802
+ msg.setSender({ addr: email.to, name: options.fromName });
803
+ msg.setRecipient(email.from);
804
+ msg.setSubject(
805
+ options.subject || `Re: ${email.headers.get("subject")}` || "No subject"
806
+ );
807
+ msg.addMessage({
808
+ contentType: options.contentType || "text/plain",
809
+ data: options.body
810
+ });
811
+
812
+ const domain = email.from.split("@")[1];
813
+ const messageId = `<${agentId}@${domain}>`;
814
+ msg.setHeader("In-Reply-To", email.headers.get("Message-ID")!);
815
+ msg.setHeader("Message-ID", messageId);
816
+ msg.setHeader("X-Agent-Name", agentName);
817
+ msg.setHeader("X-Agent-ID", agentId);
818
+
819
+ if (options.headers) {
820
+ for (const [key, value] of Object.entries(options.headers)) {
821
+ msg.setHeader(key, value);
822
+ }
823
+ }
824
+ await email.reply({
825
+ from: email.to,
826
+ raw: msg.asRaw(),
827
+ to: email.from
828
+ });
829
+ });
830
+ }
831
+
832
+ private async _tryCatch<T>(fn: () => T | Promise<T>) {
452
833
  try {
453
834
  return await fn();
454
835
  } catch (e) {
@@ -456,6 +837,68 @@ export class Agent<Env, State = unknown> extends Server<Env> {
456
837
  }
457
838
  }
458
839
 
840
+ /**
841
+ * Automatically wrap custom methods with agent context
842
+ * This ensures getCurrentAgent() works in all custom methods without decorators
843
+ */
844
+ private _autoWrapCustomMethods() {
845
+ // Collect all methods from base prototypes (Agent and Server)
846
+ const basePrototypes = [Agent.prototype, Server.prototype];
847
+ const baseMethods = new Set<string>();
848
+ for (const baseProto of basePrototypes) {
849
+ let proto = baseProto;
850
+ while (proto && proto !== Object.prototype) {
851
+ const methodNames = Object.getOwnPropertyNames(proto);
852
+ for (const methodName of methodNames) {
853
+ baseMethods.add(methodName);
854
+ }
855
+ proto = Object.getPrototypeOf(proto);
856
+ }
857
+ }
858
+ // Get all methods from the current instance's prototype chain
859
+ let proto = Object.getPrototypeOf(this);
860
+ let depth = 0;
861
+ while (proto && proto !== Object.prototype && depth < 10) {
862
+ const methodNames = Object.getOwnPropertyNames(proto);
863
+ for (const methodName of methodNames) {
864
+ const descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
865
+
866
+ // Skip if it's a private method, a base method, a getter, or not a function,
867
+ if (
868
+ baseMethods.has(methodName) ||
869
+ methodName.startsWith("_") ||
870
+ !descriptor ||
871
+ !!descriptor.get ||
872
+ typeof descriptor.value !== "function"
873
+ ) {
874
+ continue;
875
+ }
876
+
877
+ // Now, methodName is confirmed to be a custom method/function
878
+ // Wrap the custom method with context
879
+ const wrappedFunction = withAgentContext(
880
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
881
+ this[methodName as keyof this] as (...args: any[]) => any
882
+ // biome-ignore lint/suspicious/noExplicitAny: I can't typescript
883
+ ) as any;
884
+
885
+ // if the method is callable, copy the metadata from the original method
886
+ if (this._isCallable(methodName)) {
887
+ callableMetadata.set(
888
+ wrappedFunction,
889
+ callableMetadata.get(this[methodName as keyof this] as Function)!
890
+ );
891
+ }
892
+
893
+ // set the wrapped function on the prototype
894
+ this.constructor.prototype[methodName as keyof this] = wrappedFunction;
895
+ }
896
+
897
+ proto = Object.getPrototypeOf(proto);
898
+ depth++;
899
+ }
900
+ }
901
+
459
902
  override onError(
460
903
  connection: Connection,
461
904
  error: unknown
@@ -490,6 +933,131 @@ export class Agent<Env, State = unknown> extends Server<Env> {
490
933
  throw new Error("Not implemented");
491
934
  }
492
935
 
936
+ /**
937
+ * Queue a task to be executed in the future
938
+ * @param payload Payload to pass to the callback
939
+ * @param callback Name of the method to call
940
+ * @returns The ID of the queued task
941
+ */
942
+ async queue<T = unknown>(callback: keyof this, payload: T): Promise<string> {
943
+ const id = nanoid(9);
944
+ if (typeof callback !== "string") {
945
+ throw new Error("Callback must be a string");
946
+ }
947
+
948
+ if (typeof this[callback] !== "function") {
949
+ throw new Error(`this.${callback} is not a function`);
950
+ }
951
+
952
+ this.sql`
953
+ INSERT OR REPLACE INTO cf_agents_queues (id, payload, callback)
954
+ VALUES (${id}, ${JSON.stringify(payload)}, ${callback})
955
+ `;
956
+
957
+ void this._flushQueue().catch((e) => {
958
+ console.error("Error flushing queue:", e);
959
+ });
960
+
961
+ return id;
962
+ }
963
+
964
+ private _flushingQueue = false;
965
+
966
+ private async _flushQueue() {
967
+ if (this._flushingQueue) {
968
+ return;
969
+ }
970
+ this._flushingQueue = true;
971
+ while (true) {
972
+ const result = this.sql<QueueItem<string>>`
973
+ SELECT * FROM cf_agents_queues
974
+ ORDER BY created_at ASC
975
+ `;
976
+
977
+ if (!result || result.length === 0) {
978
+ break;
979
+ }
980
+
981
+ for (const row of result || []) {
982
+ const callback = this[row.callback as keyof Agent<Env>];
983
+ if (!callback) {
984
+ console.error(`callback ${row.callback} not found`);
985
+ continue;
986
+ }
987
+ const { connection, request, email } = agentContext.getStore() || {};
988
+ await agentContext.run(
989
+ {
990
+ agent: this,
991
+ connection,
992
+ request,
993
+ email
994
+ },
995
+ async () => {
996
+ // TODO: add retries and backoff
997
+ await (
998
+ callback as (
999
+ payload: unknown,
1000
+ queueItem: QueueItem<string>
1001
+ ) => Promise<void>
1002
+ ).bind(this)(JSON.parse(row.payload as string), row);
1003
+ await this.dequeue(row.id);
1004
+ }
1005
+ );
1006
+ }
1007
+ }
1008
+ this._flushingQueue = false;
1009
+ }
1010
+
1011
+ /**
1012
+ * Dequeue a task by ID
1013
+ * @param id ID of the task to dequeue
1014
+ */
1015
+ async dequeue(id: string) {
1016
+ this.sql`DELETE FROM cf_agents_queues WHERE id = ${id}`;
1017
+ }
1018
+
1019
+ /**
1020
+ * Dequeue all tasks
1021
+ */
1022
+ async dequeueAll() {
1023
+ this.sql`DELETE FROM cf_agents_queues`;
1024
+ }
1025
+
1026
+ /**
1027
+ * Dequeue all tasks by callback
1028
+ * @param callback Name of the callback to dequeue
1029
+ */
1030
+ async dequeueAllByCallback(callback: string) {
1031
+ this.sql`DELETE FROM cf_agents_queues WHERE callback = ${callback}`;
1032
+ }
1033
+
1034
+ /**
1035
+ * Get a queued task by ID
1036
+ * @param id ID of the task to get
1037
+ * @returns The task or undefined if not found
1038
+ */
1039
+ async getQueue(id: string): Promise<QueueItem<string> | undefined> {
1040
+ const result = this.sql<QueueItem<string>>`
1041
+ SELECT * FROM cf_agents_queues WHERE id = ${id}
1042
+ `;
1043
+ return result
1044
+ ? { ...result[0], payload: JSON.parse(result[0].payload) }
1045
+ : undefined;
1046
+ }
1047
+
1048
+ /**
1049
+ * Get all queues by key and value
1050
+ * @param key Key to filter by
1051
+ * @param value Value to filter by
1052
+ * @returns Array of matching QueueItem objects
1053
+ */
1054
+ async getQueues(key: string, value: string): Promise<QueueItem<string>[]> {
1055
+ const result = this.sql<QueueItem<string>>`
1056
+ SELECT * FROM cf_agents_queues
1057
+ `;
1058
+ return result.filter((row) => JSON.parse(row.payload)[key] === value);
1059
+ }
1060
+
493
1061
  /**
494
1062
  * Schedule a task to be executed in the future
495
1063
  * @template T Type of the payload data
@@ -505,6 +1073,21 @@ export class Agent<Env, State = unknown> extends Server<Env> {
505
1073
  ): Promise<Schedule<T>> {
506
1074
  const id = nanoid(9);
507
1075
 
1076
+ const emitScheduleCreate = (schedule: Schedule<T>) =>
1077
+ this.observability?.emit(
1078
+ {
1079
+ displayMessage: `Schedule ${schedule.id} created`,
1080
+ id: nanoid(),
1081
+ payload: {
1082
+ callback: callback as string,
1083
+ id: id
1084
+ },
1085
+ timestamp: Date.now(),
1086
+ type: "schedule:create"
1087
+ },
1088
+ this.ctx
1089
+ );
1090
+
508
1091
  if (typeof callback !== "string") {
509
1092
  throw new Error("Callback must be a string");
510
1093
  }
@@ -522,15 +1105,19 @@ export class Agent<Env, State = unknown> extends Server<Env> {
522
1105
  )}, 'scheduled', ${timestamp})
523
1106
  `;
524
1107
 
525
- await this.#scheduleNextAlarm();
1108
+ await this._scheduleNextAlarm();
526
1109
 
527
- return {
528
- id,
1110
+ const schedule: Schedule<T> = {
529
1111
  callback: callback,
1112
+ id,
530
1113
  payload: payload as T,
531
1114
  time: timestamp,
532
- type: "scheduled",
1115
+ type: "scheduled"
533
1116
  };
1117
+
1118
+ emitScheduleCreate(schedule);
1119
+
1120
+ return schedule;
534
1121
  }
535
1122
  if (typeof when === "number") {
536
1123
  const time = new Date(Date.now() + when * 1000);
@@ -543,16 +1130,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
543
1130
  )}, 'delayed', ${when}, ${timestamp})
544
1131
  `;
545
1132
 
546
- await this.#scheduleNextAlarm();
1133
+ await this._scheduleNextAlarm();
547
1134
 
548
- return {
549
- id,
1135
+ const schedule: Schedule<T> = {
550
1136
  callback: callback,
551
- payload: payload as T,
552
1137
  delayInSeconds: when,
1138
+ id,
1139
+ payload: payload as T,
553
1140
  time: timestamp,
554
- type: "delayed",
1141
+ type: "delayed"
555
1142
  };
1143
+
1144
+ emitScheduleCreate(schedule);
1145
+
1146
+ return schedule;
556
1147
  }
557
1148
  if (typeof when === "string") {
558
1149
  const nextExecutionTime = getNextCronTime(when);
@@ -565,16 +1156,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
565
1156
  )}, 'cron', ${when}, ${timestamp})
566
1157
  `;
567
1158
 
568
- await this.#scheduleNextAlarm();
1159
+ await this._scheduleNextAlarm();
569
1160
 
570
- return {
571
- id,
1161
+ const schedule: Schedule<T> = {
572
1162
  callback: callback,
573
- payload: payload as T,
574
1163
  cron: when,
1164
+ id,
1165
+ payload: payload as T,
575
1166
  time: timestamp,
576
- type: "cron",
1167
+ type: "cron"
577
1168
  };
1169
+
1170
+ emitScheduleCreate(schedule);
1171
+
1172
+ return schedule;
578
1173
  }
579
1174
  throw new Error("Invalid schedule type");
580
1175
  }
@@ -638,7 +1233,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
638
1233
  .toArray()
639
1234
  .map((row) => ({
640
1235
  ...row,
641
- payload: JSON.parse(row.payload as string) as T,
1236
+ payload: JSON.parse(row.payload as string) as T
642
1237
  })) as Schedule<T>[];
643
1238
 
644
1239
  return result;
@@ -650,18 +1245,34 @@ export class Agent<Env, State = unknown> extends Server<Env> {
650
1245
  * @returns true if the task was cancelled, false otherwise
651
1246
  */
652
1247
  async cancelSchedule(id: string): Promise<boolean> {
1248
+ const schedule = await this.getSchedule(id);
1249
+ if (schedule) {
1250
+ this.observability?.emit(
1251
+ {
1252
+ displayMessage: `Schedule ${id} cancelled`,
1253
+ id: nanoid(),
1254
+ payload: {
1255
+ callback: schedule.callback,
1256
+ id: schedule.id
1257
+ },
1258
+ timestamp: Date.now(),
1259
+ type: "schedule:cancel"
1260
+ },
1261
+ this.ctx
1262
+ );
1263
+ }
653
1264
  this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
654
1265
 
655
- await this.#scheduleNextAlarm();
1266
+ await this._scheduleNextAlarm();
656
1267
  return true;
657
1268
  }
658
1269
 
659
- async #scheduleNextAlarm() {
1270
+ private async _scheduleNextAlarm() {
660
1271
  // Find the next schedule that needs to be executed
661
1272
  const result = this.sql`
662
- SELECT time FROM cf_agents_schedules
1273
+ SELECT time FROM cf_agents_schedules
663
1274
  WHERE time > ${Math.floor(Date.now() / 1000)}
664
- ORDER BY time ASC
1275
+ ORDER BY time ASC
665
1276
  LIMIT 1
666
1277
  `;
667
1278
  if (!result) return;
@@ -673,10 +1284,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
673
1284
  }
674
1285
 
675
1286
  /**
676
- * Method called when an alarm fires
677
- * Executes any scheduled tasks that are due
1287
+ * Method called when an alarm fires.
1288
+ * Executes any scheduled tasks that are due.
1289
+ *
1290
+ * @remarks
1291
+ * To schedule a task, please use the `this.schedule` method instead.
1292
+ * See {@link https://developers.cloudflare.com/agents/api-reference/schedule-tasks/}
678
1293
  */
679
- async alarm() {
1294
+ public readonly alarm = async () => {
680
1295
  const now = Math.floor(Date.now() / 1000);
681
1296
 
682
1297
  // Get all schedules that should be executed now
@@ -684,46 +1299,67 @@ export class Agent<Env, State = unknown> extends Server<Env> {
684
1299
  SELECT * FROM cf_agents_schedules WHERE time <= ${now}
685
1300
  `;
686
1301
 
687
- for (const row of result || []) {
688
- const callback = this[row.callback as keyof Agent<Env>];
689
- if (!callback) {
690
- console.error(`callback ${row.callback} not found`);
691
- continue;
692
- }
693
- await unstable_context.run(
694
- { agent: this, connection: undefined, request: undefined },
695
- async () => {
696
- try {
697
- await (
698
- callback as (
699
- payload: unknown,
700
- schedule: Schedule<unknown>
701
- ) => Promise<void>
702
- ).bind(this)(JSON.parse(row.payload as string), row);
703
- } catch (e) {
704
- console.error(`error executing callback "${row.callback}"`, e);
705
- }
1302
+ if (result && Array.isArray(result)) {
1303
+ for (const row of result) {
1304
+ const callback = this[row.callback as keyof Agent<Env>];
1305
+ if (!callback) {
1306
+ console.error(`callback ${row.callback} not found`);
1307
+ continue;
706
1308
  }
707
- );
708
- if (row.type === "cron") {
709
- // Update next execution time for cron schedules
710
- const nextExecutionTime = getNextCronTime(row.cron);
711
- const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
1309
+ await agentContext.run(
1310
+ {
1311
+ agent: this,
1312
+ connection: undefined,
1313
+ request: undefined,
1314
+ email: undefined
1315
+ },
1316
+ async () => {
1317
+ try {
1318
+ this.observability?.emit(
1319
+ {
1320
+ displayMessage: `Schedule ${row.id} executed`,
1321
+ id: nanoid(),
1322
+ payload: {
1323
+ callback: row.callback,
1324
+ id: row.id
1325
+ },
1326
+ timestamp: Date.now(),
1327
+ type: "schedule:execute"
1328
+ },
1329
+ this.ctx
1330
+ );
712
1331
 
713
- this.sql`
1332
+ await (
1333
+ callback as (
1334
+ payload: unknown,
1335
+ schedule: Schedule<unknown>
1336
+ ) => Promise<void>
1337
+ ).bind(this)(JSON.parse(row.payload as string), row);
1338
+ } catch (e) {
1339
+ console.error(`error executing callback "${row.callback}"`, e);
1340
+ }
1341
+ }
1342
+ );
1343
+ if (row.type === "cron") {
1344
+ // Update next execution time for cron schedules
1345
+ const nextExecutionTime = getNextCronTime(row.cron);
1346
+ const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1000);
1347
+
1348
+ this.sql`
714
1349
  UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
715
1350
  `;
716
- } else {
717
- // Delete one-time schedules after execution
718
- this.sql`
1351
+ } else {
1352
+ // Delete one-time schedules after execution
1353
+ this.sql`
719
1354
  DELETE FROM cf_agents_schedules WHERE id = ${row.id}
720
1355
  `;
1356
+ }
721
1357
  }
722
1358
  }
723
1359
 
724
1360
  // Schedule the next alarm
725
- await this.#scheduleNextAlarm();
726
- }
1361
+ await this._scheduleNextAlarm();
1362
+ };
727
1363
 
728
1364
  /**
729
1365
  * Destroy the Agent, removing all state and scheduled tasks
@@ -732,22 +1368,208 @@ export class Agent<Env, State = unknown> extends Server<Env> {
732
1368
  // drop all tables
733
1369
  this.sql`DROP TABLE IF EXISTS cf_agents_state`;
734
1370
  this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
1371
+ this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
1372
+ this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
735
1373
 
736
1374
  // delete all alarms
737
1375
  await this.ctx.storage.deleteAlarm();
738
1376
  await this.ctx.storage.deleteAll();
1377
+ this.ctx.abort("destroyed"); // enforce that the agent is evicted
1378
+
1379
+ this.observability?.emit(
1380
+ {
1381
+ displayMessage: "Agent destroyed",
1382
+ id: nanoid(),
1383
+ payload: {},
1384
+ timestamp: Date.now(),
1385
+ type: "destroy"
1386
+ },
1387
+ this.ctx
1388
+ );
739
1389
  }
740
1390
 
741
1391
  /**
742
1392
  * Get all methods marked as callable on this Agent
743
1393
  * @returns A map of method names to their metadata
744
1394
  */
745
- #isCallable(method: string): boolean {
746
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
1395
+ private _isCallable(method: string): boolean {
747
1396
  return callableMetadata.has(this[method as keyof this] as Function);
748
1397
  }
1398
+
1399
+ /**
1400
+ * Connect to a new MCP Server
1401
+ *
1402
+ * @param url MCP Server SSE URL
1403
+ * @param callbackHost Base host for the agent, used for the redirect URI.
1404
+ * @param agentsPrefix agents routing prefix if not using `agents`
1405
+ * @param options MCP client and transport (header) options
1406
+ * @returns authUrl
1407
+ */
1408
+ async addMcpServer(
1409
+ serverName: string,
1410
+ url: string,
1411
+ callbackHost: string,
1412
+ agentsPrefix = "agents",
1413
+ options?: {
1414
+ client?: ConstructorParameters<typeof Client>[1];
1415
+ transport?: {
1416
+ headers: HeadersInit;
1417
+ };
1418
+ }
1419
+ ): Promise<{ id: string; authUrl: string | undefined }> {
1420
+ const callbackUrl = `${callbackHost}/${agentsPrefix}/${camelCaseToKebabCase(this._ParentClass.name)}/${this.name}/callback`;
1421
+
1422
+ const result = await this._connectToMcpServerInternal(
1423
+ serverName,
1424
+ url,
1425
+ callbackUrl,
1426
+ options
1427
+ );
1428
+ this.sql`
1429
+ INSERT
1430
+ OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
1431
+ VALUES (
1432
+ ${result.id},
1433
+ ${serverName},
1434
+ ${url},
1435
+ ${result.clientId ?? null},
1436
+ ${result.authUrl ?? null},
1437
+ ${callbackUrl},
1438
+ ${options ? JSON.stringify(options) : null}
1439
+ );
1440
+ `;
1441
+
1442
+ this.broadcast(
1443
+ JSON.stringify({
1444
+ mcp: this.getMcpServers(),
1445
+ type: MessageType.CF_AGENT_MCP_SERVERS
1446
+ })
1447
+ );
1448
+
1449
+ return result;
1450
+ }
1451
+
1452
+ async _connectToMcpServerInternal(
1453
+ _serverName: string,
1454
+ url: string,
1455
+ callbackUrl: string,
1456
+ // it's important that any options here are serializable because we put them into our sqlite DB for reconnection purposes
1457
+ options?: {
1458
+ client?: ConstructorParameters<typeof Client>[1];
1459
+ /**
1460
+ * We don't expose the normal set of transport options because:
1461
+ * 1) we can't serialize things like the auth provider or a fetch function into the DB for reconnection purposes
1462
+ * 2) We probably want these options to be agnostic to the transport type (SSE vs Streamable)
1463
+ *
1464
+ * This has the limitation that you can't override fetch, but I think headers should handle nearly all cases needed (i.e. non-standard bearer auth).
1465
+ */
1466
+ transport?: {
1467
+ headers?: HeadersInit;
1468
+ };
1469
+ },
1470
+ reconnect?: {
1471
+ id: string;
1472
+ oauthClientId?: string;
1473
+ }
1474
+ ): Promise<{
1475
+ id: string;
1476
+ authUrl: string | undefined;
1477
+ clientId: string | undefined;
1478
+ }> {
1479
+ const authProvider = new DurableObjectOAuthClientProvider(
1480
+ this.ctx.storage,
1481
+ this.name,
1482
+ callbackUrl
1483
+ );
1484
+
1485
+ if (reconnect) {
1486
+ authProvider.serverId = reconnect.id;
1487
+ if (reconnect.oauthClientId) {
1488
+ authProvider.clientId = reconnect.oauthClientId;
1489
+ }
1490
+ }
1491
+
1492
+ // allows passing through transport headers if necessary
1493
+ // this handles some non-standard bearer auth setups (i.e. MCP server behind CF access instead of OAuth)
1494
+ let headerTransportOpts: SSEClientTransportOptions = {};
1495
+ if (options?.transport?.headers) {
1496
+ headerTransportOpts = {
1497
+ eventSourceInit: {
1498
+ fetch: (url, init) =>
1499
+ fetch(url, {
1500
+ ...init,
1501
+ headers: options?.transport?.headers
1502
+ })
1503
+ },
1504
+ requestInit: {
1505
+ headers: options?.transport?.headers
1506
+ }
1507
+ };
1508
+ }
1509
+
1510
+ const { id, authUrl, clientId } = await this.mcp.connect(url, {
1511
+ client: options?.client,
1512
+ reconnect,
1513
+ transport: {
1514
+ ...headerTransportOpts,
1515
+ authProvider
1516
+ }
1517
+ });
1518
+
1519
+ return {
1520
+ authUrl,
1521
+ clientId,
1522
+ id
1523
+ };
1524
+ }
1525
+
1526
+ async removeMcpServer(id: string) {
1527
+ this.mcp.closeConnection(id);
1528
+ this.sql`
1529
+ DELETE FROM cf_agents_mcp_servers WHERE id = ${id};
1530
+ `;
1531
+ this.broadcast(
1532
+ JSON.stringify({
1533
+ mcp: this.getMcpServers(),
1534
+ type: MessageType.CF_AGENT_MCP_SERVERS
1535
+ })
1536
+ );
1537
+ }
1538
+
1539
+ getMcpServers(): MCPServersState {
1540
+ const mcpState: MCPServersState = {
1541
+ prompts: this.mcp.listPrompts(),
1542
+ resources: this.mcp.listResources(),
1543
+ servers: {},
1544
+ tools: this.mcp.listTools()
1545
+ };
1546
+
1547
+ const servers = this.sql<MCPServerRow>`
1548
+ SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
1549
+ `;
1550
+
1551
+ if (servers && Array.isArray(servers) && servers.length > 0) {
1552
+ for (const server of servers) {
1553
+ const serverConn = this.mcp.mcpConnections[server.id];
1554
+ mcpState.servers[server.id] = {
1555
+ auth_url: server.auth_url,
1556
+ capabilities: serverConn?.serverCapabilities ?? null,
1557
+ instructions: serverConn?.instructions ?? null,
1558
+ name: server.name,
1559
+ server_url: server.server_url,
1560
+ // mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
1561
+ state: serverConn?.connectionState ?? "authenticating"
1562
+ };
1563
+ }
1564
+ }
1565
+
1566
+ return mcpState;
1567
+ }
749
1568
  }
750
1569
 
1570
+ // A set of classes that have been wrapped with agent context
1571
+ const wrappedClasses = new Set<typeof Agent.prototype.constructor>();
1572
+
751
1573
  /**
752
1574
  * Namespace for creating Agent instances
753
1575
  * @template Agentic Type of the Agent class
@@ -785,17 +1607,17 @@ export async function routeAgentRequest<Env>(
785
1607
  const corsHeaders =
786
1608
  options?.cors === true
787
1609
  ? {
788
- "Access-Control-Allow-Origin": "*",
789
- "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
790
1610
  "Access-Control-Allow-Credentials": "true",
791
- "Access-Control-Max-Age": "86400",
1611
+ "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
1612
+ "Access-Control-Allow-Origin": "*",
1613
+ "Access-Control-Max-Age": "86400"
792
1614
  }
793
1615
  : options?.cors;
794
1616
 
795
1617
  if (request.method === "OPTIONS") {
796
1618
  if (corsHeaders) {
797
1619
  return new Response(null, {
798
- headers: corsHeaders,
1620
+ headers: corsHeaders
799
1621
  });
800
1622
  }
801
1623
  console.warn(
@@ -808,7 +1630,7 @@ export async function routeAgentRequest<Env>(
808
1630
  env as Record<string, unknown>,
809
1631
  {
810
1632
  prefix: "agents",
811
- ...(options as PartyServerOptions<Record<string, unknown>>),
1633
+ ...(options as PartyServerOptions<Record<string, unknown>>)
812
1634
  }
813
1635
  );
814
1636
 
@@ -821,24 +1643,238 @@ export async function routeAgentRequest<Env>(
821
1643
  response = new Response(response.body, {
822
1644
  headers: {
823
1645
  ...response.headers,
824
- ...corsHeaders,
825
- },
1646
+ ...corsHeaders
1647
+ }
826
1648
  });
827
1649
  }
828
1650
  return response;
829
1651
  }
830
1652
 
1653
+ export type EmailResolver<Env> = (
1654
+ email: ForwardableEmailMessage,
1655
+ env: Env
1656
+ ) => Promise<{
1657
+ agentName: string;
1658
+ agentId: string;
1659
+ } | null>;
1660
+
1661
+ /**
1662
+ * Create a resolver that uses the message-id header to determine the agent to route the email to
1663
+ * @returns A function that resolves the agent to route the email to
1664
+ */
1665
+ export function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {
1666
+ return async (email: ForwardableEmailMessage, _env: Env) => {
1667
+ const messageId = email.headers.get("message-id");
1668
+ if (messageId) {
1669
+ const messageIdMatch = messageId.match(/<([^@]+)@([^>]+)>/);
1670
+ if (messageIdMatch) {
1671
+ const [, agentId, domain] = messageIdMatch;
1672
+ const agentName = domain.split(".")[0];
1673
+ return { agentName, agentId };
1674
+ }
1675
+ }
1676
+
1677
+ const references = email.headers.get("references");
1678
+ if (references) {
1679
+ const referencesMatch = references.match(
1680
+ /<([A-Za-z0-9+/]{43}=)@([^>]+)>/
1681
+ );
1682
+ if (referencesMatch) {
1683
+ const [, base64Id, domain] = referencesMatch;
1684
+ const agentId = Buffer.from(base64Id, "base64").toString("hex");
1685
+ const agentName = domain.split(".")[0];
1686
+ return { agentName, agentId };
1687
+ }
1688
+ }
1689
+
1690
+ const agentName = email.headers.get("x-agent-name");
1691
+ const agentId = email.headers.get("x-agent-id");
1692
+ if (agentName && agentId) {
1693
+ return { agentName, agentId };
1694
+ }
1695
+
1696
+ return null;
1697
+ };
1698
+ }
1699
+
1700
+ /**
1701
+ * Create a resolver that uses the email address to determine the agent to route the email to
1702
+ * @param defaultAgentName The default agent name to use if the email address does not contain a sub-address
1703
+ * @returns A function that resolves the agent to route the email to
1704
+ */
1705
+ export function createAddressBasedEmailResolver<Env>(
1706
+ defaultAgentName: string
1707
+ ): EmailResolver<Env> {
1708
+ return async (email: ForwardableEmailMessage, _env: Env) => {
1709
+ const emailMatch = email.to.match(/^([^+@]+)(?:\+([^@]+))?@(.+)$/);
1710
+ if (!emailMatch) {
1711
+ return null;
1712
+ }
1713
+
1714
+ const [, localPart, subAddress] = emailMatch;
1715
+
1716
+ if (subAddress) {
1717
+ return {
1718
+ agentName: localPart,
1719
+ agentId: subAddress
1720
+ };
1721
+ }
1722
+
1723
+ // Option 2: Use defaultAgentName namespace, localPart as agentId
1724
+ // Common for catch-all email routing to a single EmailAgent namespace
1725
+ return {
1726
+ agentName: defaultAgentName,
1727
+ agentId: localPart
1728
+ };
1729
+ };
1730
+ }
1731
+
1732
+ /**
1733
+ * Create a resolver that uses the agentName and agentId to determine the agent to route the email to
1734
+ * @param agentName The name of the agent to route the email to
1735
+ * @param agentId The id of the agent to route the email to
1736
+ * @returns A function that resolves the agent to route the email to
1737
+ */
1738
+ export function createCatchAllEmailResolver<Env>(
1739
+ agentName: string,
1740
+ agentId: string
1741
+ ): EmailResolver<Env> {
1742
+ return async () => ({ agentName, agentId });
1743
+ }
1744
+
1745
+ export type EmailRoutingOptions<Env> = AgentOptions<Env> & {
1746
+ resolver: EmailResolver<Env>;
1747
+ };
1748
+
1749
+ // Cache the agent namespace map for email routing
1750
+ // This maps both kebab-case and original names to namespaces
1751
+ const agentMapCache = new WeakMap<
1752
+ Record<string, unknown>,
1753
+ Record<string, unknown>
1754
+ >();
1755
+
831
1756
  /**
832
1757
  * Route an email to the appropriate Agent
833
- * @param email Email message to route
834
- * @param env Environment containing Agent bindings
835
- * @param options Routing options
1758
+ * @param email The email to route
1759
+ * @param env The environment containing the Agent bindings
1760
+ * @param options The options for routing the email
1761
+ * @returns A promise that resolves when the email has been routed
836
1762
  */
837
1763
  export async function routeAgentEmail<Env>(
838
1764
  email: ForwardableEmailMessage,
839
1765
  env: Env,
840
- options?: AgentOptions<Env>
841
- ): Promise<void> {}
1766
+ options: EmailRoutingOptions<Env>
1767
+ ): Promise<void> {
1768
+ const routingInfo = await options.resolver(email, env);
1769
+
1770
+ if (!routingInfo) {
1771
+ console.warn("No routing information found for email, dropping message");
1772
+ return;
1773
+ }
1774
+
1775
+ // Build a map that includes both original names and kebab-case versions
1776
+ if (!agentMapCache.has(env as Record<string, unknown>)) {
1777
+ const map: Record<string, unknown> = {};
1778
+ for (const [key, value] of Object.entries(env as Record<string, unknown>)) {
1779
+ if (
1780
+ value &&
1781
+ typeof value === "object" &&
1782
+ "idFromName" in value &&
1783
+ typeof value.idFromName === "function"
1784
+ ) {
1785
+ // Add both the original name and kebab-case version
1786
+ map[key] = value;
1787
+ map[camelCaseToKebabCase(key)] = value;
1788
+ }
1789
+ }
1790
+ agentMapCache.set(env as Record<string, unknown>, map);
1791
+ }
1792
+
1793
+ const agentMap = agentMapCache.get(env as Record<string, unknown>)!;
1794
+ const namespace = agentMap[routingInfo.agentName];
1795
+
1796
+ if (!namespace) {
1797
+ // Provide helpful error message listing available agents
1798
+ const availableAgents = Object.keys(agentMap)
1799
+ .filter((key) => !key.includes("-")) // Show only original names, not kebab-case duplicates
1800
+ .join(", ");
1801
+ throw new Error(
1802
+ `Agent namespace '${routingInfo.agentName}' not found in environment. Available agents: ${availableAgents}`
1803
+ );
1804
+ }
1805
+
1806
+ const agent = await getAgentByName(
1807
+ namespace as unknown as AgentNamespace<Agent<Env>>,
1808
+ routingInfo.agentId
1809
+ );
1810
+
1811
+ // let's make a serialisable version of the email
1812
+ const serialisableEmail: AgentEmail = {
1813
+ getRaw: async () => {
1814
+ const reader = email.raw.getReader();
1815
+ const chunks: Uint8Array[] = [];
1816
+
1817
+ let done = false;
1818
+ while (!done) {
1819
+ const { value, done: readerDone } = await reader.read();
1820
+ done = readerDone;
1821
+ if (value) {
1822
+ chunks.push(value);
1823
+ }
1824
+ }
1825
+
1826
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
1827
+ const combined = new Uint8Array(totalLength);
1828
+ let offset = 0;
1829
+ for (const chunk of chunks) {
1830
+ combined.set(chunk, offset);
1831
+ offset += chunk.length;
1832
+ }
1833
+
1834
+ return combined;
1835
+ },
1836
+ headers: email.headers,
1837
+ rawSize: email.rawSize,
1838
+ setReject: (reason: string) => {
1839
+ email.setReject(reason);
1840
+ },
1841
+ forward: (rcptTo: string, headers?: Headers) => {
1842
+ return email.forward(rcptTo, headers);
1843
+ },
1844
+ reply: (options: { from: string; to: string; raw: string }) => {
1845
+ return email.reply(
1846
+ new EmailMessage(options.from, options.to, options.raw)
1847
+ );
1848
+ },
1849
+ from: email.from,
1850
+ to: email.to
1851
+ };
1852
+
1853
+ await agent._onEmail(serialisableEmail);
1854
+ }
1855
+
1856
+ export type AgentEmail = {
1857
+ from: string;
1858
+ to: string;
1859
+ getRaw: () => Promise<Uint8Array>;
1860
+ headers: Headers;
1861
+ rawSize: number;
1862
+ setReject: (reason: string) => void;
1863
+ forward: (rcptTo: string, headers?: Headers) => Promise<void>;
1864
+ reply: (options: { from: string; to: string; raw: string }) => Promise<void>;
1865
+ };
1866
+
1867
+ export type EmailSendOptions = {
1868
+ to: string;
1869
+ subject: string;
1870
+ body: string;
1871
+ contentType?: string;
1872
+ headers?: Record<string, string>;
1873
+ includeRoutingHeaders?: boolean;
1874
+ agentName?: string;
1875
+ agentId?: string;
1876
+ domain?: string;
1877
+ };
842
1878
 
843
1879
  /**
844
1880
  * Get or create an Agent by name
@@ -849,12 +1885,17 @@ export async function routeAgentEmail<Env>(
849
1885
  * @param options Options for Agent creation
850
1886
  * @returns Promise resolving to an Agent instance stub
851
1887
  */
852
- export function getAgentByName<Env, T extends Agent<Env>>(
1888
+ export async function getAgentByName<
1889
+ Env,
1890
+ T extends Agent<Env>,
1891
+ Props extends Record<string, unknown> = Record<string, unknown>
1892
+ >(
853
1893
  namespace: AgentNamespace<T>,
854
1894
  name: string,
855
1895
  options?: {
856
1896
  jurisdiction?: DurableObjectJurisdiction;
857
1897
  locationHint?: DurableObjectLocationHint;
1898
+ props?: Props;
858
1899
  }
859
1900
  ) {
860
1901
  return getServerByName<Env, T>(namespace, name, options);
@@ -864,13 +1905,13 @@ export function getAgentByName<Env, T extends Agent<Env>>(
864
1905
  * A wrapper for streaming responses in callable methods
865
1906
  */
866
1907
  export class StreamingResponse {
867
- #connection: Connection;
868
- #id: string;
869
- #closed = false;
1908
+ private _connection: Connection;
1909
+ private _id: string;
1910
+ private _closed = false;
870
1911
 
871
1912
  constructor(connection: Connection, id: string) {
872
- this.#connection = connection;
873
- this.#id = id;
1913
+ this._connection = connection;
1914
+ this._id = id;
874
1915
  }
875
1916
 
876
1917
  /**
@@ -878,17 +1919,17 @@ export class StreamingResponse {
878
1919
  * @param chunk The data to send
879
1920
  */
880
1921
  send(chunk: unknown) {
881
- if (this.#closed) {
1922
+ if (this._closed) {
882
1923
  throw new Error("StreamingResponse is already closed");
883
1924
  }
884
1925
  const response: RPCResponse = {
885
- type: "rpc",
886
- id: this.#id,
887
- success: true,
888
- result: chunk,
889
1926
  done: false,
1927
+ id: this._id,
1928
+ result: chunk,
1929
+ success: true,
1930
+ type: MessageType.RPC
890
1931
  };
891
- this.#connection.send(JSON.stringify(response));
1932
+ this._connection.send(JSON.stringify(response));
892
1933
  }
893
1934
 
894
1935
  /**
@@ -896,17 +1937,17 @@ export class StreamingResponse {
896
1937
  * @param finalChunk Optional final chunk of data to send
897
1938
  */
898
1939
  end(finalChunk?: unknown) {
899
- if (this.#closed) {
1940
+ if (this._closed) {
900
1941
  throw new Error("StreamingResponse is already closed");
901
1942
  }
902
- this.#closed = true;
1943
+ this._closed = true;
903
1944
  const response: RPCResponse = {
904
- type: "rpc",
905
- id: this.#id,
906
- success: true,
907
- result: finalChunk,
908
1945
  done: true,
1946
+ id: this._id,
1947
+ result: finalChunk,
1948
+ success: true,
1949
+ type: MessageType.RPC
909
1950
  };
910
- this.#connection.send(JSON.stringify(response));
1951
+ this._connection.send(JSON.stringify(response));
911
1952
  }
912
1953
  }