agents 0.0.0-7ff0509 → 0.0.0-8157d08

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 (50) hide show
  1. package/dist/ai-chat-agent.d.ts +51 -5
  2. package/dist/ai-chat-agent.js +179 -78
  3. package/dist/ai-chat-agent.js.map +1 -1
  4. package/dist/ai-react.d.ts +17 -4
  5. package/dist/ai-react.js +62 -48
  6. package/dist/ai-react.js.map +1 -1
  7. package/dist/ai-types.d.ts +5 -0
  8. package/dist/chunk-767EASBA.js +106 -0
  9. package/dist/chunk-767EASBA.js.map +1 -0
  10. package/dist/chunk-E3LCYPCB.js +469 -0
  11. package/dist/chunk-E3LCYPCB.js.map +1 -0
  12. package/dist/chunk-JFRK72K3.js +910 -0
  13. package/dist/chunk-JFRK72K3.js.map +1 -0
  14. package/dist/chunk-NKZZ66QY.js +116 -0
  15. package/dist/chunk-NKZZ66QY.js.map +1 -0
  16. package/dist/client.d.ts +15 -1
  17. package/dist/client.js +6 -133
  18. package/dist/client.js.map +1 -1
  19. package/dist/index-CITGJflw.d.ts +486 -0
  20. package/dist/index.d.ts +29 -307
  21. package/dist/index.js +8 -8
  22. package/dist/mcp/client.d.ts +1055 -0
  23. package/dist/mcp/client.js +9 -0
  24. package/dist/mcp/do-oauth-client-provider.d.ts +41 -0
  25. package/dist/mcp/do-oauth-client-provider.js +7 -0
  26. package/dist/mcp/do-oauth-client-provider.js.map +1 -0
  27. package/dist/mcp/index.d.ts +84 -0
  28. package/dist/mcp/index.js +783 -0
  29. package/dist/mcp/index.js.map +1 -0
  30. package/dist/observability/index.d.ts +12 -0
  31. package/dist/observability/index.js +10 -0
  32. package/dist/observability/index.js.map +1 -0
  33. package/dist/react.d.ts +85 -5
  34. package/dist/react.js +50 -31
  35. package/dist/react.js.map +1 -1
  36. package/dist/schedule.d.ts +6 -6
  37. package/dist/schedule.js +4 -6
  38. package/dist/schedule.js.map +1 -1
  39. package/dist/serializable.d.ts +32 -0
  40. package/dist/serializable.js +1 -0
  41. package/dist/serializable.js.map +1 -0
  42. package/package.json +83 -51
  43. package/src/index.ts +542 -102
  44. package/dist/chunk-HMLY7DHA.js +0 -16
  45. package/dist/chunk-KRBQHBPA.js +0 -599
  46. package/dist/chunk-KRBQHBPA.js.map +0 -1
  47. package/dist/mcp.d.ts +0 -58
  48. package/dist/mcp.js +0 -945
  49. package/dist/mcp.js.map +0 -1
  50. /package/dist/{chunk-HMLY7DHA.js.map → mcp/client.js.map} +0 -0
package/src/index.ts CHANGED
@@ -1,21 +1,31 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
4
+
5
+ import type {
6
+ Prompt,
7
+ Resource,
8
+ ServerCapabilities,
9
+ Tool,
10
+ } from "@modelcontextprotocol/sdk/types.js";
11
+ import { parseCronExpression } from "cron-schedule";
12
+ import { nanoid } from "nanoid";
1
13
  import {
2
- Server,
3
- routePartykitRequest,
4
- type PartyServerOptions,
5
- getServerByName,
6
14
  type Connection,
7
15
  type ConnectionContext,
16
+ getServerByName,
17
+ type PartyServerOptions,
18
+ routePartykitRequest,
19
+ Server,
8
20
  type WSMessage,
9
21
  } from "partyserver";
22
+ import { camelCaseToKebabCase } from "./client";
23
+ import { MCPClientManager } from "./mcp/client";
24
+ // import type { MCPClientConnection } from "./mcp/client-connection";
25
+ import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider";
26
+ import { genericObservability, type Observability } from "./observability";
10
27
 
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";
17
-
18
- import { WorkflowEntrypoint as CFWorkflowEntrypoint } from "cloudflare:workers";
28
+ export type { Connection, ConnectionContext, WSMessage } from "partyserver";
19
29
 
20
30
  /**
21
31
  * RPC request message from client
@@ -99,7 +109,6 @@ export type CallableMetadata = {
99
109
  streaming?: boolean;
100
110
  };
101
111
 
102
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
103
112
  const callableMetadata = new Map<Function, CallableMetadata>();
104
113
 
105
114
  /**
@@ -109,6 +118,7 @@ const callableMetadata = new Map<Function, CallableMetadata>();
109
118
  export function unstable_callable(metadata: CallableMetadata = {}) {
110
119
  return function callableDecorator<This, Args extends unknown[], Return>(
111
120
  target: (this: This, ...args: Args) => Return,
121
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: later
112
122
  context: ClassMethodDecoratorContext
113
123
  ) {
114
124
  if (!callableMetadata.has(target)) {
@@ -119,11 +129,6 @@ export function unstable_callable(metadata: CallableMetadata = {}) {
119
129
  };
120
130
  }
121
131
 
122
- /**
123
- * A class for creating workflow entry points that can be used with Cloudflare Workers
124
- */
125
- export class WorkflowEntrypoint extends CFWorkflowEntrypoint {}
126
-
127
132
  /**
128
133
  * Represents a scheduled task within an Agent
129
134
  * @template T Type of the payload data
@@ -165,24 +170,95 @@ function getNextCronTime(cron: string) {
165
170
  return interval.getNextDate();
166
171
  }
167
172
 
173
+ /**
174
+ * MCP Server state update message from server -> Client
175
+ */
176
+ export type MCPServerMessage = {
177
+ type: "cf_agent_mcp_servers";
178
+ mcp: MCPServersState;
179
+ };
180
+
181
+ export type MCPServersState = {
182
+ servers: {
183
+ [id: string]: MCPServer;
184
+ };
185
+ tools: Tool[];
186
+ prompts: Prompt[];
187
+ resources: Resource[];
188
+ };
189
+
190
+ export type MCPServer = {
191
+ name: string;
192
+ server_url: string;
193
+ auth_url: string | null;
194
+ // This state is specifically about the temporary process of getting a token (if needed).
195
+ // Scope outside of that can't be relied upon because when the DO sleeps, there's no way
196
+ // to communicate a change to a non-ready state.
197
+ state: "authenticating" | "connecting" | "ready" | "discovering" | "failed";
198
+ instructions: string | null;
199
+ capabilities: ServerCapabilities | null;
200
+ };
201
+
202
+ /**
203
+ * MCP Server data stored in DO SQL for resuming MCP Server connections
204
+ */
205
+ type MCPServerRow = {
206
+ id: string;
207
+ name: string;
208
+ server_url: string;
209
+ client_id: string | null;
210
+ auth_url: string | null;
211
+ callback_url: string;
212
+ server_options: string;
213
+ };
214
+
168
215
  const STATE_ROW_ID = "cf_state_row_id";
169
216
  const STATE_WAS_CHANGED = "cf_state_was_changed";
170
217
 
171
218
  const DEFAULT_STATE = {} as unknown;
172
219
 
173
- export const unstable_context = new AsyncLocalStorage<{
220
+ const agentContext = new AsyncLocalStorage<{
174
221
  agent: Agent<unknown>;
175
222
  connection: Connection | undefined;
176
223
  request: Request | undefined;
177
224
  }>();
178
225
 
226
+ export function getCurrentAgent<
227
+ T extends Agent<unknown, unknown> = Agent<unknown, unknown>,
228
+ >(): {
229
+ agent: T | undefined;
230
+ connection: Connection | undefined;
231
+ request: Request<unknown, CfProperties<unknown>> | undefined;
232
+ } {
233
+ const store = agentContext.getStore() as
234
+ | {
235
+ agent: T;
236
+ connection: Connection | undefined;
237
+ request: Request<unknown, CfProperties<unknown>> | undefined;
238
+ }
239
+ | undefined;
240
+ if (!store) {
241
+ return {
242
+ agent: undefined,
243
+ connection: undefined,
244
+ request: undefined,
245
+ };
246
+ }
247
+ return store;
248
+ }
249
+
179
250
  /**
180
251
  * Base class for creating Agent implementations
181
252
  * @template Env Environment type containing bindings
182
253
  * @template State State type to store within the Agent
183
254
  */
184
255
  export class Agent<Env, State = unknown> extends Server<Env> {
185
- #state = DEFAULT_STATE as State;
256
+ private _state = DEFAULT_STATE as State;
257
+
258
+ private _ParentClass: typeof Agent<Env, State> =
259
+ Object.getPrototypeOf(this).constructor;
260
+
261
+ mcp: MCPClientManager = new MCPClientManager(this._ParentClass.name, "0.0.1");
186
262
 
187
263
  /**
188
264
  * Initial state for the Agent
@@ -194,9 +270,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
194
270
  * Current state of the Agent
195
271
  */
196
272
  get state(): State {
197
- if (this.#state !== DEFAULT_STATE) {
273
+ if (this._state !== DEFAULT_STATE) {
198
274
  // state was previously set, and populated internal state
199
- return this.#state;
275
+ return this._state;
200
276
  }
201
277
  // looks like this is the first time the state is being accessed
202
278
  // check if the state was set in a previous life
@@ -216,8 +292,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
216
292
  ) {
217
293
  const state = result[0]?.state as string; // could be null?
218
294
 
219
- this.#state = JSON.parse(state);
220
- return this.#state;
295
+ this._state = JSON.parse(state);
296
+ return this._state;
221
297
  }
222
298
 
223
299
  // ok, this is the first time the state is being accessed
@@ -241,6 +317,11 @@ export class Agent<Env, State = unknown> extends Server<Env> {
241
317
  hibernate: true, // default to hibernate
242
318
  };
243
319
 
320
+ /**
321
+ * The observability implementation to use for the Agent
322
+ */
323
+ observability?: Observability = genericObservability;
324
+
244
325
  /**
245
326
  * Execute SQL queries against the Agent's database
246
327
  * @template T Type of the returned rows
@@ -278,7 +359,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
278
359
  `;
279
360
 
280
361
  void this.ctx.blockConcurrencyWhile(async () => {
281
- return this.#tryCatch(async () => {
362
+ return this._tryCatch(async () => {
282
363
  // Create alarms table if it doesn't exist
283
364
  this.sql`
284
365
  CREATE TABLE IF NOT EXISTS cf_agents_schedules (
@@ -298,25 +379,65 @@ export class Agent<Env, State = unknown> extends Server<Env> {
298
379
  });
299
380
  });
300
381
 
382
+ this.sql`
383
+ CREATE TABLE IF NOT EXISTS cf_agents_mcp_servers (
384
+ id TEXT PRIMARY KEY NOT NULL,
385
+ name TEXT NOT NULL,
386
+ server_url TEXT NOT NULL,
387
+ callback_url TEXT NOT NULL,
388
+ client_id TEXT,
389
+ auth_url TEXT,
390
+ server_options TEXT
391
+ )
392
+ `;
393
+
394
+ const _onRequest = this.onRequest.bind(this);
395
+ this.onRequest = (request: Request) => {
396
+ return agentContext.run(
397
+ { agent: this, connection: undefined, request },
398
+ async () => {
399
+ if (this.mcp.isCallbackRequest(request)) {
400
+ await this.mcp.handleCallbackRequest(request);
401
+
402
+ // after the MCP connection handshake, we can send updated mcp state
403
+ this.broadcast(
404
+ JSON.stringify({
405
+ mcp: this.getMcpServers(),
406
+ type: "cf_agent_mcp_servers",
407
+ })
408
+ );
409
+
410
+ // We probably should let the user configure this response/redirect, but this is fine for now.
411
+ return new Response("<script>window.close();</script>", {
412
+ headers: { "content-type": "text/html" },
413
+ status: 200,
414
+ });
415
+ }
416
+
417
+ return this._tryCatch(() => _onRequest(request));
418
+ }
419
+ );
420
+ };
421
+
301
422
  const _onMessage = this.onMessage.bind(this);
302
423
  this.onMessage = async (connection: Connection, message: WSMessage) => {
303
- return unstable_context.run(
424
+ return agentContext.run(
304
425
  { agent: this, connection, request: undefined },
305
426
  async () => {
306
427
  if (typeof message !== "string") {
307
- return this.#tryCatch(() => _onMessage(connection, message));
428
+ return this._tryCatch(() => _onMessage(connection, message));
308
429
  }
309
430
 
310
431
  let parsed: unknown;
311
432
  try {
312
433
  parsed = JSON.parse(message);
313
- } catch (e) {
434
+ } catch (_e) {
314
435
  // silently fail and let the onMessage handler handle it
315
- return this.#tryCatch(() => _onMessage(connection, message));
436
+ return this._tryCatch(() => _onMessage(connection, message));
316
437
  }
317
438
 
318
439
  if (isStateUpdateMessage(parsed)) {
319
- this.#setStateInternal(parsed.state as State, connection);
440
+ this._setStateInternal(parsed.state as State, connection);
320
441
  return;
321
442
  }
322
443
 
@@ -330,11 +451,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
330
451
  throw new Error(`Method ${method} does not exist`);
331
452
  }
332
453
 
333
- if (!this.#isCallable(method)) {
454
+ if (!this._isCallable(method)) {
334
455
  throw new Error(`Method ${method} is not callable`);
335
456
  }
336
457
 
337
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
338
458
  const metadata = callableMetadata.get(methodFn as Function);
339
459
 
340
460
  // For streaming methods, pass a StreamingResponse object
@@ -346,22 +466,39 @@ export class Agent<Env, State = unknown> extends Server<Env> {
346
466
 
347
467
  // For regular methods, execute and send response
348
468
  const result = await methodFn.apply(this, args);
469
+
470
+ this.observability?.emit(
471
+ {
472
+ displayMessage: `RPC call to ${method}`,
473
+ id: nanoid(),
474
+ payload: {
475
+ args,
476
+ method,
477
+ streaming: metadata?.streaming,
478
+ success: true,
479
+ },
480
+ timestamp: Date.now(),
481
+ type: "rpc",
482
+ },
483
+ this.ctx
484
+ );
485
+
349
486
  const response: RPCResponse = {
350
- type: "rpc",
487
+ done: true,
351
488
  id,
352
- success: true,
353
489
  result,
354
- done: true,
490
+ success: true,
491
+ type: "rpc",
355
492
  };
356
493
  connection.send(JSON.stringify(response));
357
494
  } catch (e) {
358
495
  // Send error response
359
496
  const response: RPCResponse = {
360
- type: "rpc",
361
- id: parsed.id,
362
- success: false,
363
497
  error:
364
498
  e instanceof Error ? e.message : "Unknown error occurred",
499
+ id: parsed.id,
500
+ success: false,
501
+ type: "rpc",
365
502
  };
366
503
  connection.send(JSON.stringify(response));
367
504
  console.error("RPC error:", e);
@@ -369,7 +506,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
369
506
  return;
370
507
  }
371
508
 
372
- return this.#tryCatch(() => _onMessage(connection, message));
509
+ return this._tryCatch(() => _onMessage(connection, message));
373
510
  }
374
511
  );
375
512
  };
@@ -378,27 +515,89 @@ export class Agent<Env, State = unknown> extends Server<Env> {
378
515
  this.onConnect = (connection: Connection, ctx: ConnectionContext) => {
379
516
  // TODO: This is a hack to ensure the state is sent after the connection is established
380
517
  // must fix this
381
- return unstable_context.run(
518
+ return agentContext.run(
382
519
  { agent: this, connection, request: ctx.request },
383
520
  async () => {
384
521
  setTimeout(() => {
385
522
  if (this.state) {
386
523
  connection.send(
387
524
  JSON.stringify({
388
- type: "cf_agent_state",
389
525
  state: this.state,
526
+ type: "cf_agent_state",
390
527
  })
391
528
  );
392
529
  }
393
- return this.#tryCatch(() => _onConnect(connection, ctx));
530
+
531
+ connection.send(
532
+ JSON.stringify({
533
+ mcp: this.getMcpServers(),
534
+ type: "cf_agent_mcp_servers",
535
+ })
536
+ );
537
+
538
+ this.observability?.emit(
539
+ {
540
+ displayMessage: "Connection established",
541
+ id: nanoid(),
542
+ payload: {
543
+ connectionId: connection.id,
544
+ },
545
+ timestamp: Date.now(),
546
+ type: "connect",
547
+ },
548
+ this.ctx
549
+ );
550
+ return this._tryCatch(() => _onConnect(connection, ctx));
394
551
  }, 20);
395
552
  }
396
553
  );
397
554
  };
555
+
556
+ const _onStart = this.onStart.bind(this);
557
+ this.onStart = async () => {
558
+ return agentContext.run(
559
+ { agent: this, connection: undefined, request: undefined },
560
+ async () => {
561
+ const servers = this.sql<MCPServerRow>`
562
+ SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
563
+ `;
564
+
565
+ // from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
566
+ Promise.allSettled(
567
+ servers.map((server) => {
568
+ return this._connectToMcpServerInternal(
569
+ server.name,
570
+ server.server_url,
571
+ server.callback_url,
572
+ server.server_options
573
+ ? JSON.parse(server.server_options)
574
+ : undefined,
575
+ {
576
+ id: server.id,
577
+ oauthClientId: server.client_id ?? undefined,
578
+ }
579
+ );
580
+ })
581
+ ).then((_results) => {
582
+ this.broadcast(
583
+ JSON.stringify({
584
+ mcp: this.getMcpServers(),
585
+ type: "cf_agent_mcp_servers",
586
+ })
587
+ );
588
+ });
589
+ await this._tryCatch(() => _onStart());
590
+ }
591
+ );
592
+ };
398
593
  }
399
594
 
400
- #setStateInternal(state: State, source: Connection | "server" = "server") {
401
- this.#state = state;
595
+ private _setStateInternal(
596
+ state: State,
597
+ source: Connection | "server" = "server"
598
+ ) {
599
+ const previousState = this._state;
600
+ this._state = state;
402
601
  this.sql`
403
602
  INSERT OR REPLACE INTO cf_agents_state (id, state)
404
603
  VALUES (${STATE_ROW_ID}, ${JSON.stringify(state)})
@@ -409,16 +608,29 @@ export class Agent<Env, State = unknown> extends Server<Env> {
409
608
  `;
410
609
  this.broadcast(
411
610
  JSON.stringify({
412
- type: "cf_agent_state",
413
611
  state: state,
612
+ type: "cf_agent_state",
414
613
  }),
415
614
  source !== "server" ? [source.id] : []
416
615
  );
417
- return this.#tryCatch(() => {
418
- const { connection, request } = unstable_context.getStore() || {};
419
- return unstable_context.run(
616
+ return this._tryCatch(() => {
617
+ const { connection, request } = agentContext.getStore() || {};
618
+ return agentContext.run(
420
619
  { agent: this, connection, request },
421
620
  async () => {
621
+ this.observability?.emit(
622
+ {
623
+ displayMessage: "State updated",
624
+ id: nanoid(),
625
+ payload: {
626
+ previousState,
627
+ state,
628
+ },
629
+ timestamp: Date.now(),
630
+ type: "state:update",
631
+ },
632
+ this.ctx
633
+ );
422
634
  return this.onStateUpdate(state, source);
423
635
  }
424
636
  );
@@ -430,7 +642,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
430
642
  * @param state New state to set
431
643
  */
432
644
  setState(state: State) {
433
- this.#setStateInternal(state, "server");
645
+ this._setStateInternal(state, "server");
434
646
  }
435
647
 
436
648
  /**
@@ -438,6 +650,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
438
650
  * @param state Updated state
439
651
  * @param source Source of the state update ("server" or a client connection)
440
652
  */
653
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
441
654
  onStateUpdate(state: State | undefined, source: Connection | "server") {
442
655
  // override this to handle state updates
443
656
  }
@@ -446,8 +659,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
446
659
  * Called when the Agent receives an email
447
660
  * @param email Email message to process
448
661
  */
662
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
449
663
  onEmail(email: ForwardableEmailMessage) {
450
- return unstable_context.run(
664
+ return agentContext.run(
451
665
  { agent: this, connection: undefined, request: undefined },
452
666
  async () => {
453
667
  console.error("onEmail not implemented");
@@ -455,7 +669,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
455
669
  );
456
670
  }
457
671
 
458
- async #tryCatch<T>(fn: () => T | Promise<T>) {
672
+ private async _tryCatch<T>(fn: () => T | Promise<T>) {
459
673
  try {
460
674
  return await fn();
461
675
  } catch (e) {
@@ -512,6 +726,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
512
726
  ): Promise<Schedule<T>> {
513
727
  const id = nanoid(9);
514
728
 
729
+ const emitScheduleCreate = (schedule: Schedule<T>) =>
730
+ this.observability?.emit(
731
+ {
732
+ displayMessage: `Schedule ${schedule.id} created`,
733
+ id: nanoid(),
734
+ payload: schedule,
735
+ timestamp: Date.now(),
736
+ type: "schedule:create",
737
+ },
738
+ this.ctx
739
+ );
740
+
515
741
  if (typeof callback !== "string") {
516
742
  throw new Error("Callback must be a string");
517
743
  }
@@ -529,15 +755,19 @@ export class Agent<Env, State = unknown> extends Server<Env> {
529
755
  )}, 'scheduled', ${timestamp})
530
756
  `;
531
757
 
532
- await this.#scheduleNextAlarm();
758
+ await this._scheduleNextAlarm();
533
759
 
534
- return {
535
- id,
760
+ const schedule: Schedule<T> = {
536
761
  callback: callback,
762
+ id,
537
763
  payload: payload as T,
538
764
  time: timestamp,
539
765
  type: "scheduled",
540
766
  };
767
+
768
+ emitScheduleCreate(schedule);
769
+
770
+ return schedule;
541
771
  }
542
772
  if (typeof when === "number") {
543
773
  const time = new Date(Date.now() + when * 1000);
@@ -550,16 +780,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
550
780
  )}, 'delayed', ${when}, ${timestamp})
551
781
  `;
552
782
 
553
- await this.#scheduleNextAlarm();
783
+ await this._scheduleNextAlarm();
554
784
 
555
- return {
556
- id,
785
+ const schedule: Schedule<T> = {
557
786
  callback: callback,
558
- payload: payload as T,
559
787
  delayInSeconds: when,
788
+ id,
789
+ payload: payload as T,
560
790
  time: timestamp,
561
791
  type: "delayed",
562
792
  };
793
+
794
+ emitScheduleCreate(schedule);
795
+
796
+ return schedule;
563
797
  }
564
798
  if (typeof when === "string") {
565
799
  const nextExecutionTime = getNextCronTime(when);
@@ -572,16 +806,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
572
806
  )}, 'cron', ${when}, ${timestamp})
573
807
  `;
574
808
 
575
- await this.#scheduleNextAlarm();
809
+ await this._scheduleNextAlarm();
576
810
 
577
- return {
578
- id,
811
+ const schedule: Schedule<T> = {
579
812
  callback: callback,
580
- payload: payload as T,
581
813
  cron: when,
814
+ id,
815
+ payload: payload as T,
582
816
  time: timestamp,
583
817
  type: "cron",
584
818
  };
819
+
820
+ emitScheduleCreate(schedule);
821
+
822
+ return schedule;
585
823
  }
586
824
  throw new Error("Invalid schedule type");
587
825
  }
@@ -612,7 +850,6 @@ export class Agent<Env, State = unknown> extends Server<Env> {
612
850
  */
613
851
  getSchedules<T = string>(
614
852
  criteria: {
615
- description?: string;
616
853
  id?: string;
617
854
  type?: "scheduled" | "delayed" | "cron";
618
855
  timeRange?: { start?: Date; end?: Date };
@@ -626,11 +863,6 @@ export class Agent<Env, State = unknown> extends Server<Env> {
626
863
  params.push(criteria.id);
627
864
  }
628
865
 
629
- if (criteria.description) {
630
- query += " AND description = ?";
631
- params.push(criteria.description);
632
- }
633
-
634
866
  if (criteria.type) {
635
867
  query += " AND type = ?";
636
868
  params.push(criteria.type);
@@ -663,13 +895,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
663
895
  * @returns true if the task was cancelled, false otherwise
664
896
  */
665
897
  async cancelSchedule(id: string): Promise<boolean> {
898
+ const schedule = await this.getSchedule(id);
899
+ if (schedule) {
900
+ this.observability?.emit(
901
+ {
902
+ displayMessage: `Schedule ${id} cancelled`,
903
+ id: nanoid(),
904
+ payload: schedule,
905
+ timestamp: Date.now(),
906
+ type: "schedule:cancel",
907
+ },
908
+ this.ctx
909
+ );
910
+ }
666
911
  this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
667
912
 
668
- await this.#scheduleNextAlarm();
913
+ await this._scheduleNextAlarm();
669
914
  return true;
670
915
  }
671
916
 
672
- async #scheduleNextAlarm() {
917
+ private async _scheduleNextAlarm() {
673
918
  // Find the next schedule that needs to be executed
674
919
  const result = this.sql`
675
920
  SELECT time FROM cf_agents_schedules
@@ -686,10 +931,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
686
931
  }
687
932
 
688
933
  /**
689
- * Method called when an alarm fires
690
- * Executes any scheduled tasks that are due
934
+ * Method called when an alarm fires.
935
+ * Executes any scheduled tasks that are due.
936
+ *
937
+ * @remarks
938
+ * To schedule a task, please use the `this.schedule` method instead.
939
+ * See {@link https://developers.cloudflare.com/agents/api-reference/schedule-tasks/}
691
940
  */
692
- async alarm() {
941
+ public readonly alarm = async () => {
693
942
  const now = Math.floor(Date.now() / 1000);
694
943
 
695
944
  // Get all schedules that should be executed now
@@ -703,10 +952,21 @@ export class Agent<Env, State = unknown> extends Server<Env> {
703
952
  console.error(`callback ${row.callback} not found`);
704
953
  continue;
705
954
  }
706
- await unstable_context.run(
955
+ await agentContext.run(
707
956
  { agent: this, connection: undefined, request: undefined },
708
957
  async () => {
709
958
  try {
959
+ this.observability?.emit(
960
+ {
961
+ displayMessage: `Schedule ${row.id} executed`,
962
+ id: nanoid(),
963
+ payload: row,
964
+ timestamp: Date.now(),
965
+ type: "schedule:execute",
966
+ },
967
+ this.ctx
968
+ );
969
+
710
970
  await (
711
971
  callback as (
712
972
  payload: unknown,
@@ -735,8 +995,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
735
995
  }
736
996
 
737
997
  // Schedule the next alarm
738
- await this.#scheduleNextAlarm();
739
- }
998
+ await this._scheduleNextAlarm();
999
+ };
740
1000
 
741
1001
  /**
742
1002
  * Destroy the Agent, removing all state and scheduled tasks
@@ -745,20 +1005,200 @@ export class Agent<Env, State = unknown> extends Server<Env> {
745
1005
  // drop all tables
746
1006
  this.sql`DROP TABLE IF EXISTS cf_agents_state`;
747
1007
  this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
1008
+ this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
748
1009
 
749
1010
  // delete all alarms
750
1011
  await this.ctx.storage.deleteAlarm();
751
1012
  await this.ctx.storage.deleteAll();
1013
+ this.ctx.abort("destroyed"); // enforce that the agent is evicted
1014
+
1015
+ this.observability?.emit(
1016
+ {
1017
+ displayMessage: "Agent destroyed",
1018
+ id: nanoid(),
1019
+ payload: {},
1020
+ timestamp: Date.now(),
1021
+ type: "destroy",
1022
+ },
1023
+ this.ctx
1024
+ );
752
1025
  }
753
1026
 
754
1027
  /**
755
1028
  * Get all methods marked as callable on this Agent
756
1029
  * @returns A map of method names to their metadata
757
1030
  */
758
- #isCallable(method: string): boolean {
759
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
1031
+ private _isCallable(method: string): boolean {
760
1032
  return callableMetadata.has(this[method as keyof this] as Function);
761
1033
  }
1034
+
1035
+ /**
1036
+ * Connect to a new MCP Server
1037
+ *
1038
+ * @param url MCP Server SSE URL
1039
+ * @param callbackHost Base host for the agent, used for the redirect URI.
1040
+ * @param agentsPrefix agents routing prefix if not using `agents`
1041
+ * @param options MCP client and transport (header) options
1042
+ * @returns authUrl
1043
+ */
1044
+ async addMcpServer(
1045
+ serverName: string,
1046
+ url: string,
1047
+ callbackHost: string,
1048
+ agentsPrefix = "agents",
1049
+ options?: {
1050
+ client?: ConstructorParameters<typeof Client>[1];
1051
+ transport?: {
1052
+ headers: HeadersInit;
1053
+ };
1054
+ }
1055
+ ): Promise<{ id: string; authUrl: string | undefined }> {
1056
+ const callbackUrl = `${callbackHost}/${agentsPrefix}/${camelCaseToKebabCase(this._ParentClass.name)}/${this.name}/callback`;
1057
+
1058
+ const result = await this._connectToMcpServerInternal(
1059
+ serverName,
1060
+ url,
1061
+ callbackUrl,
1062
+ options
1063
+ );
1064
+ this.sql`
1065
+ INSERT
1066
+ OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
1067
+ VALUES (
1068
+ ${result.id},
1069
+ ${serverName},
1070
+ ${url},
1071
+ ${result.clientId ?? null},
1072
+ ${result.authUrl ?? null},
1073
+ ${callbackUrl},
1074
+ ${options ? JSON.stringify(options) : null}
1075
+ );
1076
+ `;
1077
+
1078
+ this.broadcast(
1079
+ JSON.stringify({
1080
+ mcp: this.getMcpServers(),
1081
+ type: "cf_agent_mcp_servers",
1082
+ })
1083
+ );
1084
+
1085
+ return result;
1086
+ }
1087
+
1088
+ async _connectToMcpServerInternal(
1089
+ _serverName: string,
1090
+ url: string,
1091
+ callbackUrl: string,
1092
+ // it's important that any options here are serializable because we put them into our sqlite DB for reconnection purposes
1093
+ options?: {
1094
+ client?: ConstructorParameters<typeof Client>[1];
1095
+ /**
1096
+ * We don't expose the normal set of transport options because:
1097
+ * 1) we can't serialize things like the auth provider or a fetch function into the DB for reconnection purposes
1098
+ * 2) We probably want these options to be agnostic to the transport type (SSE vs Streamable)
1099
+ *
1100
+ * 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).
1101
+ */
1102
+ transport?: {
1103
+ headers?: HeadersInit;
1104
+ };
1105
+ },
1106
+ reconnect?: {
1107
+ id: string;
1108
+ oauthClientId?: string;
1109
+ }
1110
+ ): Promise<{
1111
+ id: string;
1112
+ authUrl: string | undefined;
1113
+ clientId: string | undefined;
1114
+ }> {
1115
+ const authProvider = new DurableObjectOAuthClientProvider(
1116
+ this.ctx.storage,
1117
+ this.name,
1118
+ callbackUrl
1119
+ );
1120
+
1121
+ if (reconnect) {
1122
+ authProvider.serverId = reconnect.id;
1123
+ if (reconnect.oauthClientId) {
1124
+ authProvider.clientId = reconnect.oauthClientId;
1125
+ }
1126
+ }
1127
+
1128
+ // allows passing through transport headers if necessary
1129
+ // this handles some non-standard bearer auth setups (i.e. MCP server behind CF access instead of OAuth)
1130
+ let headerTransportOpts: SSEClientTransportOptions = {};
1131
+ if (options?.transport?.headers) {
1132
+ headerTransportOpts = {
1133
+ eventSourceInit: {
1134
+ fetch: (url, init) =>
1135
+ fetch(url, {
1136
+ ...init,
1137
+ headers: options?.transport?.headers,
1138
+ }),
1139
+ },
1140
+ requestInit: {
1141
+ headers: options?.transport?.headers,
1142
+ },
1143
+ };
1144
+ }
1145
+
1146
+ const { id, authUrl, clientId } = await this.mcp.connect(url, {
1147
+ client: options?.client,
1148
+ reconnect,
1149
+ transport: {
1150
+ ...headerTransportOpts,
1151
+ authProvider,
1152
+ },
1153
+ });
1154
+
1155
+ return {
1156
+ authUrl,
1157
+ clientId,
1158
+ id,
1159
+ };
1160
+ }
1161
+
1162
+ async removeMcpServer(id: string) {
1163
+ this.mcp.closeConnection(id);
1164
+ this.sql`
1165
+ DELETE FROM cf_agents_mcp_servers WHERE id = ${id};
1166
+ `;
1167
+ this.broadcast(
1168
+ JSON.stringify({
1169
+ mcp: this.getMcpServers(),
1170
+ type: "cf_agent_mcp_servers",
1171
+ })
1172
+ );
1173
+ }
1174
+
1175
+ getMcpServers(): MCPServersState {
1176
+ const mcpState: MCPServersState = {
1177
+ prompts: this.mcp.listPrompts(),
1178
+ resources: this.mcp.listResources(),
1179
+ servers: {},
1180
+ tools: this.mcp.listTools(),
1181
+ };
1182
+
1183
+ const servers = this.sql<MCPServerRow>`
1184
+ SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
1185
+ `;
1186
+
1187
+ for (const server of servers) {
1188
+ const serverConn = this.mcp.mcpConnections[server.id];
1189
+ mcpState.servers[server.id] = {
1190
+ auth_url: server.auth_url,
1191
+ capabilities: serverConn?.serverCapabilities ?? null,
1192
+ instructions: serverConn?.instructions ?? null,
1193
+ name: server.name,
1194
+ server_url: server.server_url,
1195
+ // mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
1196
+ state: serverConn?.connectionState ?? "authenticating",
1197
+ };
1198
+ }
1199
+
1200
+ return mcpState;
1201
+ }
762
1202
  }
763
1203
 
764
1204
  /**
@@ -798,9 +1238,9 @@ export async function routeAgentRequest<Env>(
798
1238
  const corsHeaders =
799
1239
  options?.cors === true
800
1240
  ? {
801
- "Access-Control-Allow-Origin": "*",
802
- "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
803
1241
  "Access-Control-Allow-Credentials": "true",
1242
+ "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
1243
+ "Access-Control-Allow-Origin": "*",
804
1244
  "Access-Control-Max-Age": "86400",
805
1245
  }
806
1246
  : options?.cors;
@@ -848,9 +1288,9 @@ export async function routeAgentRequest<Env>(
848
1288
  * @param options Routing options
849
1289
  */
850
1290
  export async function routeAgentEmail<Env>(
851
- email: ForwardableEmailMessage,
852
- env: Env,
853
- options?: AgentOptions<Env>
1291
+ _email: ForwardableEmailMessage,
1292
+ _env: Env,
1293
+ _options?: AgentOptions<Env>
854
1294
  ): Promise<void> {}
855
1295
 
856
1296
  /**
@@ -862,7 +1302,7 @@ export async function routeAgentEmail<Env>(
862
1302
  * @param options Options for Agent creation
863
1303
  * @returns Promise resolving to an Agent instance stub
864
1304
  */
865
- export function getAgentByName<Env, T extends Agent<Env>>(
1305
+ export async function getAgentByName<Env, T extends Agent<Env>>(
866
1306
  namespace: AgentNamespace<T>,
867
1307
  name: string,
868
1308
  options?: {
@@ -877,13 +1317,13 @@ export function getAgentByName<Env, T extends Agent<Env>>(
877
1317
  * A wrapper for streaming responses in callable methods
878
1318
  */
879
1319
  export class StreamingResponse {
880
- #connection: Connection;
881
- #id: string;
882
- #closed = false;
1320
+ private _connection: Connection;
1321
+ private _id: string;
1322
+ private _closed = false;
883
1323
 
884
1324
  constructor(connection: Connection, id: string) {
885
- this.#connection = connection;
886
- this.#id = id;
1325
+ this._connection = connection;
1326
+ this._id = id;
887
1327
  }
888
1328
 
889
1329
  /**
@@ -891,17 +1331,17 @@ export class StreamingResponse {
891
1331
  * @param chunk The data to send
892
1332
  */
893
1333
  send(chunk: unknown) {
894
- if (this.#closed) {
1334
+ if (this._closed) {
895
1335
  throw new Error("StreamingResponse is already closed");
896
1336
  }
897
1337
  const response: RPCResponse = {
898
- type: "rpc",
899
- id: this.#id,
900
- success: true,
901
- result: chunk,
902
1338
  done: false,
1339
+ id: this._id,
1340
+ result: chunk,
1341
+ success: true,
1342
+ type: "rpc",
903
1343
  };
904
- this.#connection.send(JSON.stringify(response));
1344
+ this._connection.send(JSON.stringify(response));
905
1345
  }
906
1346
 
907
1347
  /**
@@ -909,17 +1349,17 @@ export class StreamingResponse {
909
1349
  * @param finalChunk Optional final chunk of data to send
910
1350
  */
911
1351
  end(finalChunk?: unknown) {
912
- if (this.#closed) {
1352
+ if (this._closed) {
913
1353
  throw new Error("StreamingResponse is already closed");
914
1354
  }
915
- this.#closed = true;
1355
+ this._closed = true;
916
1356
  const response: RPCResponse = {
917
- type: "rpc",
918
- id: this.#id,
919
- success: true,
920
- result: finalChunk,
921
1357
  done: true,
1358
+ id: this._id,
1359
+ result: finalChunk,
1360
+ success: true,
1361
+ type: "rpc",
922
1362
  };
923
- this.#connection.send(JSON.stringify(response));
1363
+ this._connection.send(JSON.stringify(response));
924
1364
  }
925
1365
  }