agents 0.0.0-dc0e8de → 0.0.0-dc7a99c

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 (45) hide show
  1. package/dist/ai-chat-agent.d.ts +32 -5
  2. package/dist/ai-chat-agent.js +149 -115
  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 +28 -29
  6. package/dist/ai-react.js.map +1 -1
  7. package/dist/chunk-767EASBA.js +106 -0
  8. package/dist/chunk-767EASBA.js.map +1 -0
  9. package/dist/{chunk-Q5ZBHY4Z.js → chunk-E3LCYPCB.js} +49 -36
  10. package/dist/chunk-E3LCYPCB.js.map +1 -0
  11. package/dist/{chunk-HD4VEHBA.js → chunk-JFRK72K3.js} +463 -161
  12. package/dist/chunk-JFRK72K3.js.map +1 -0
  13. package/dist/chunk-NKZZ66QY.js +116 -0
  14. package/dist/chunk-NKZZ66QY.js.map +1 -0
  15. package/dist/client.d.ts +15 -1
  16. package/dist/client.js +6 -126
  17. package/dist/client.js.map +1 -1
  18. package/dist/index-CITGJflw.d.ts +486 -0
  19. package/dist/index.d.ts +25 -308
  20. package/dist/index.js +4 -3
  21. package/dist/mcp/client.d.ts +301 -23
  22. package/dist/mcp/client.js +1 -2
  23. package/dist/mcp/do-oauth-client-provider.d.ts +3 -3
  24. package/dist/mcp/do-oauth-client-provider.js +3 -103
  25. package/dist/mcp/do-oauth-client-provider.js.map +1 -1
  26. package/dist/mcp/index.d.ts +17 -7
  27. package/dist/mcp/index.js +147 -173
  28. package/dist/mcp/index.js.map +1 -1
  29. package/dist/observability/index.d.ts +12 -0
  30. package/dist/observability/index.js +10 -0
  31. package/dist/react.d.ts +85 -5
  32. package/dist/react.js +20 -8
  33. package/dist/react.js.map +1 -1
  34. package/dist/schedule.d.ts +6 -6
  35. package/dist/schedule.js +4 -6
  36. package/dist/schedule.js.map +1 -1
  37. package/dist/serializable.d.ts +32 -0
  38. package/dist/serializable.js +1 -0
  39. package/dist/serializable.js.map +1 -0
  40. package/package.json +75 -68
  41. package/src/index.ts +506 -83
  42. package/dist/chunk-HD4VEHBA.js.map +0 -1
  43. package/dist/chunk-HMLY7DHA.js +0 -16
  44. package/dist/chunk-Q5ZBHY4Z.js.map +0 -1
  45. /package/dist/{chunk-HMLY7DHA.js.map → observability/index.js.map} +0 -0
package/src/index.ts CHANGED
@@ -1,20 +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";
10
-
11
- import { parseCronExpression } from "cron-schedule";
12
- import { nanoid } from "nanoid";
13
-
14
- import { AsyncLocalStorage } from "node:async_hooks";
22
+ import { camelCaseToKebabCase } from "./client";
15
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";
16
27
 
17
- export type { Connection, WSMessage, ConnectionContext } from "partyserver";
28
+ export type { Connection, ConnectionContext, WSMessage } from "partyserver";
18
29
 
19
30
  /**
20
31
  * RPC request message from client
@@ -98,7 +109,6 @@ export type CallableMetadata = {
98
109
  streaming?: boolean;
99
110
  };
100
111
 
101
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
102
112
  const callableMetadata = new Map<Function, CallableMetadata>();
103
113
 
104
114
  /**
@@ -108,6 +118,7 @@ const callableMetadata = new Map<Function, CallableMetadata>();
108
118
  export function unstable_callable(metadata: CallableMetadata = {}) {
109
119
  return function callableDecorator<This, Args extends unknown[], Return>(
110
120
  target: (this: This, ...args: Args) => Return,
121
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: later
111
122
  context: ClassMethodDecoratorContext
112
123
  ) {
113
124
  if (!callableMetadata.has(target)) {
@@ -159,6 +170,48 @@ function getNextCronTime(cron: string) {
159
170
  return interval.getNextDate();
160
171
  }
161
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
+
162
215
  const STATE_ROW_ID = "cf_state_row_id";
163
216
  const STATE_WAS_CHANGED = "cf_state_was_changed";
164
217
 
@@ -200,12 +253,12 @@ export function getCurrentAgent<
200
253
  * @template State State type to store within the Agent
201
254
  */
202
255
  export class Agent<Env, State = unknown> extends Server<Env> {
203
- #state = DEFAULT_STATE as State;
256
+ private _state = DEFAULT_STATE as State;
204
257
 
205
- #ParentClass: typeof Agent<Env, State> =
258
+ private _ParentClass: typeof Agent<Env, State> =
206
259
  Object.getPrototypeOf(this).constructor;
207
260
 
208
- mcp: MCPClientManager = new MCPClientManager(this.#ParentClass.name, "0.0.1");
261
+ mcp: MCPClientManager = new MCPClientManager(this._ParentClass.name, "0.0.1");
209
262
 
210
263
  /**
211
264
  * Initial state for the Agent
@@ -217,9 +270,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
217
270
  * Current state of the Agent
218
271
  */
219
272
  get state(): State {
220
- if (this.#state !== DEFAULT_STATE) {
273
+ if (this._state !== DEFAULT_STATE) {
221
274
  // state was previously set, and populated internal state
222
- return this.#state;
275
+ return this._state;
223
276
  }
224
277
  // looks like this is the first time the state is being accessed
225
278
  // check if the state was set in a previous life
@@ -239,8 +292,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
239
292
  ) {
240
293
  const state = result[0]?.state as string; // could be null?
241
294
 
242
- this.#state = JSON.parse(state);
243
- return this.#state;
295
+ this._state = JSON.parse(state);
296
+ return this._state;
244
297
  }
245
298
 
246
299
  // ok, this is the first time the state is being accessed
@@ -264,6 +317,11 @@ export class Agent<Env, State = unknown> extends Server<Env> {
264
317
  hibernate: true, // default to hibernate
265
318
  };
266
319
 
320
+ /**
321
+ * The observability implementation to use for the Agent
322
+ */
323
+ observability?: Observability = genericObservability;
324
+
267
325
  /**
268
326
  * Execute SQL queries against the Agent's database
269
327
  * @template T Type of the returned rows
@@ -301,7 +359,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
301
359
  `;
302
360
 
303
361
  void this.ctx.blockConcurrencyWhile(async () => {
304
- return this.#tryCatch(async () => {
362
+ return this._tryCatch(async () => {
305
363
  // Create alarms table if it doesn't exist
306
364
  this.sql`
307
365
  CREATE TABLE IF NOT EXISTS cf_agents_schedules (
@@ -321,25 +379,65 @@ export class Agent<Env, State = unknown> extends Server<Env> {
321
379
  });
322
380
  });
323
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
+
324
422
  const _onMessage = this.onMessage.bind(this);
325
423
  this.onMessage = async (connection: Connection, message: WSMessage) => {
326
424
  return agentContext.run(
327
425
  { agent: this, connection, request: undefined },
328
426
  async () => {
329
427
  if (typeof message !== "string") {
330
- return this.#tryCatch(() => _onMessage(connection, message));
428
+ return this._tryCatch(() => _onMessage(connection, message));
331
429
  }
332
430
 
333
431
  let parsed: unknown;
334
432
  try {
335
433
  parsed = JSON.parse(message);
336
- } catch (e) {
434
+ } catch (_e) {
337
435
  // silently fail and let the onMessage handler handle it
338
- return this.#tryCatch(() => _onMessage(connection, message));
436
+ return this._tryCatch(() => _onMessage(connection, message));
339
437
  }
340
438
 
341
439
  if (isStateUpdateMessage(parsed)) {
342
- this.#setStateInternal(parsed.state as State, connection);
440
+ this._setStateInternal(parsed.state as State, connection);
343
441
  return;
344
442
  }
345
443
 
@@ -353,11 +451,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
353
451
  throw new Error(`Method ${method} does not exist`);
354
452
  }
355
453
 
356
- if (!this.#isCallable(method)) {
454
+ if (!this._isCallable(method)) {
357
455
  throw new Error(`Method ${method} is not callable`);
358
456
  }
359
457
 
360
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
361
458
  const metadata = callableMetadata.get(methodFn as Function);
362
459
 
363
460
  // For streaming methods, pass a StreamingResponse object
@@ -369,22 +466,39 @@ export class Agent<Env, State = unknown> extends Server<Env> {
369
466
 
370
467
  // For regular methods, execute and send response
371
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
+
372
486
  const response: RPCResponse = {
373
- type: "rpc",
487
+ done: true,
374
488
  id,
375
- success: true,
376
489
  result,
377
- done: true,
490
+ success: true,
491
+ type: "rpc",
378
492
  };
379
493
  connection.send(JSON.stringify(response));
380
494
  } catch (e) {
381
495
  // Send error response
382
496
  const response: RPCResponse = {
383
- type: "rpc",
384
- id: parsed.id,
385
- success: false,
386
497
  error:
387
498
  e instanceof Error ? e.message : "Unknown error occurred",
499
+ id: parsed.id,
500
+ success: false,
501
+ type: "rpc",
388
502
  };
389
503
  connection.send(JSON.stringify(response));
390
504
  console.error("RPC error:", e);
@@ -392,7 +506,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
392
506
  return;
393
507
  }
394
508
 
395
- return this.#tryCatch(() => _onMessage(connection, message));
509
+ return this._tryCatch(() => _onMessage(connection, message));
396
510
  }
397
511
  );
398
512
  };
@@ -408,20 +522,82 @@ export class Agent<Env, State = unknown> extends Server<Env> {
408
522
  if (this.state) {
409
523
  connection.send(
410
524
  JSON.stringify({
411
- type: "cf_agent_state",
412
525
  state: this.state,
526
+ type: "cf_agent_state",
413
527
  })
414
528
  );
415
529
  }
416
- 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));
417
551
  }, 20);
418
552
  }
419
553
  );
420
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
+ };
421
593
  }
422
594
 
423
- #setStateInternal(state: State, source: Connection | "server" = "server") {
424
- this.#state = state;
595
+ private _setStateInternal(
596
+ state: State,
597
+ source: Connection | "server" = "server"
598
+ ) {
599
+ const previousState = this._state;
600
+ this._state = state;
425
601
  this.sql`
426
602
  INSERT OR REPLACE INTO cf_agents_state (id, state)
427
603
  VALUES (${STATE_ROW_ID}, ${JSON.stringify(state)})
@@ -432,16 +608,29 @@ export class Agent<Env, State = unknown> extends Server<Env> {
432
608
  `;
433
609
  this.broadcast(
434
610
  JSON.stringify({
435
- type: "cf_agent_state",
436
611
  state: state,
612
+ type: "cf_agent_state",
437
613
  }),
438
614
  source !== "server" ? [source.id] : []
439
615
  );
440
- return this.#tryCatch(() => {
616
+ return this._tryCatch(() => {
441
617
  const { connection, request } = agentContext.getStore() || {};
442
618
  return agentContext.run(
443
619
  { agent: this, connection, request },
444
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
+ );
445
634
  return this.onStateUpdate(state, source);
446
635
  }
447
636
  );
@@ -453,7 +642,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
453
642
  * @param state New state to set
454
643
  */
455
644
  setState(state: State) {
456
- this.#setStateInternal(state, "server");
645
+ this._setStateInternal(state, "server");
457
646
  }
458
647
 
459
648
  /**
@@ -461,6 +650,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
461
650
  * @param state Updated state
462
651
  * @param source Source of the state update ("server" or a client connection)
463
652
  */
653
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
464
654
  onStateUpdate(state: State | undefined, source: Connection | "server") {
465
655
  // override this to handle state updates
466
656
  }
@@ -469,6 +659,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
469
659
  * Called when the Agent receives an email
470
660
  * @param email Email message to process
471
661
  */
662
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
472
663
  onEmail(email: ForwardableEmailMessage) {
473
664
  return agentContext.run(
474
665
  { agent: this, connection: undefined, request: undefined },
@@ -478,7 +669,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
478
669
  );
479
670
  }
480
671
 
481
- async #tryCatch<T>(fn: () => T | Promise<T>) {
672
+ private async _tryCatch<T>(fn: () => T | Promise<T>) {
482
673
  try {
483
674
  return await fn();
484
675
  } catch (e) {
@@ -535,6 +726,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
535
726
  ): Promise<Schedule<T>> {
536
727
  const id = nanoid(9);
537
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
+
538
741
  if (typeof callback !== "string") {
539
742
  throw new Error("Callback must be a string");
540
743
  }
@@ -552,15 +755,19 @@ export class Agent<Env, State = unknown> extends Server<Env> {
552
755
  )}, 'scheduled', ${timestamp})
553
756
  `;
554
757
 
555
- await this.#scheduleNextAlarm();
758
+ await this._scheduleNextAlarm();
556
759
 
557
- return {
558
- id,
760
+ const schedule: Schedule<T> = {
559
761
  callback: callback,
762
+ id,
560
763
  payload: payload as T,
561
764
  time: timestamp,
562
765
  type: "scheduled",
563
766
  };
767
+
768
+ emitScheduleCreate(schedule);
769
+
770
+ return schedule;
564
771
  }
565
772
  if (typeof when === "number") {
566
773
  const time = new Date(Date.now() + when * 1000);
@@ -573,16 +780,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
573
780
  )}, 'delayed', ${when}, ${timestamp})
574
781
  `;
575
782
 
576
- await this.#scheduleNextAlarm();
783
+ await this._scheduleNextAlarm();
577
784
 
578
- return {
579
- id,
785
+ const schedule: Schedule<T> = {
580
786
  callback: callback,
581
- payload: payload as T,
582
787
  delayInSeconds: when,
788
+ id,
789
+ payload: payload as T,
583
790
  time: timestamp,
584
791
  type: "delayed",
585
792
  };
793
+
794
+ emitScheduleCreate(schedule);
795
+
796
+ return schedule;
586
797
  }
587
798
  if (typeof when === "string") {
588
799
  const nextExecutionTime = getNextCronTime(when);
@@ -595,16 +806,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
595
806
  )}, 'cron', ${when}, ${timestamp})
596
807
  `;
597
808
 
598
- await this.#scheduleNextAlarm();
809
+ await this._scheduleNextAlarm();
599
810
 
600
- return {
601
- id,
811
+ const schedule: Schedule<T> = {
602
812
  callback: callback,
603
- payload: payload as T,
604
813
  cron: when,
814
+ id,
815
+ payload: payload as T,
605
816
  time: timestamp,
606
817
  type: "cron",
607
818
  };
819
+
820
+ emitScheduleCreate(schedule);
821
+
822
+ return schedule;
608
823
  }
609
824
  throw new Error("Invalid schedule type");
610
825
  }
@@ -680,13 +895,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
680
895
  * @returns true if the task was cancelled, false otherwise
681
896
  */
682
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
+ }
683
911
  this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
684
912
 
685
- await this.#scheduleNextAlarm();
913
+ await this._scheduleNextAlarm();
686
914
  return true;
687
915
  }
688
916
 
689
- async #scheduleNextAlarm() {
917
+ private async _scheduleNextAlarm() {
690
918
  // Find the next schedule that needs to be executed
691
919
  const result = this.sql`
692
920
  SELECT time FROM cf_agents_schedules
@@ -703,10 +931,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
703
931
  }
704
932
 
705
933
  /**
706
- * Method called when an alarm fires
707
- * 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/}
708
940
  */
709
- async alarm() {
941
+ public readonly alarm = async () => {
710
942
  const now = Math.floor(Date.now() / 1000);
711
943
 
712
944
  // Get all schedules that should be executed now
@@ -724,6 +956,17 @@ export class Agent<Env, State = unknown> extends Server<Env> {
724
956
  { agent: this, connection: undefined, request: undefined },
725
957
  async () => {
726
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
+
727
970
  await (
728
971
  callback as (
729
972
  payload: unknown,
@@ -752,8 +995,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
752
995
  }
753
996
 
754
997
  // Schedule the next alarm
755
- await this.#scheduleNextAlarm();
756
- }
998
+ await this._scheduleNextAlarm();
999
+ };
757
1000
 
758
1001
  /**
759
1002
  * Destroy the Agent, removing all state and scheduled tasks
@@ -762,20 +1005,200 @@ export class Agent<Env, State = unknown> extends Server<Env> {
762
1005
  // drop all tables
763
1006
  this.sql`DROP TABLE IF EXISTS cf_agents_state`;
764
1007
  this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
1008
+ this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
765
1009
 
766
1010
  // delete all alarms
767
1011
  await this.ctx.storage.deleteAlarm();
768
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
+ );
769
1025
  }
770
1026
 
771
1027
  /**
772
1028
  * Get all methods marked as callable on this Agent
773
1029
  * @returns A map of method names to their metadata
774
1030
  */
775
- #isCallable(method: string): boolean {
776
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
1031
+ private _isCallable(method: string): boolean {
777
1032
  return callableMetadata.has(this[method as keyof this] as Function);
778
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
+ }
779
1202
  }
780
1203
 
781
1204
  /**
@@ -815,9 +1238,9 @@ export async function routeAgentRequest<Env>(
815
1238
  const corsHeaders =
816
1239
  options?.cors === true
817
1240
  ? {
818
- "Access-Control-Allow-Origin": "*",
819
- "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
820
1241
  "Access-Control-Allow-Credentials": "true",
1242
+ "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
1243
+ "Access-Control-Allow-Origin": "*",
821
1244
  "Access-Control-Max-Age": "86400",
822
1245
  }
823
1246
  : options?.cors;
@@ -865,9 +1288,9 @@ export async function routeAgentRequest<Env>(
865
1288
  * @param options Routing options
866
1289
  */
867
1290
  export async function routeAgentEmail<Env>(
868
- email: ForwardableEmailMessage,
869
- env: Env,
870
- options?: AgentOptions<Env>
1291
+ _email: ForwardableEmailMessage,
1292
+ _env: Env,
1293
+ _options?: AgentOptions<Env>
871
1294
  ): Promise<void> {}
872
1295
 
873
1296
  /**
@@ -894,13 +1317,13 @@ export async function getAgentByName<Env, T extends Agent<Env>>(
894
1317
  * A wrapper for streaming responses in callable methods
895
1318
  */
896
1319
  export class StreamingResponse {
897
- #connection: Connection;
898
- #id: string;
899
- #closed = false;
1320
+ private _connection: Connection;
1321
+ private _id: string;
1322
+ private _closed = false;
900
1323
 
901
1324
  constructor(connection: Connection, id: string) {
902
- this.#connection = connection;
903
- this.#id = id;
1325
+ this._connection = connection;
1326
+ this._id = id;
904
1327
  }
905
1328
 
906
1329
  /**
@@ -908,17 +1331,17 @@ export class StreamingResponse {
908
1331
  * @param chunk The data to send
909
1332
  */
910
1333
  send(chunk: unknown) {
911
- if (this.#closed) {
1334
+ if (this._closed) {
912
1335
  throw new Error("StreamingResponse is already closed");
913
1336
  }
914
1337
  const response: RPCResponse = {
915
- type: "rpc",
916
- id: this.#id,
917
- success: true,
918
- result: chunk,
919
1338
  done: false,
1339
+ id: this._id,
1340
+ result: chunk,
1341
+ success: true,
1342
+ type: "rpc",
920
1343
  };
921
- this.#connection.send(JSON.stringify(response));
1344
+ this._connection.send(JSON.stringify(response));
922
1345
  }
923
1346
 
924
1347
  /**
@@ -926,17 +1349,17 @@ export class StreamingResponse {
926
1349
  * @param finalChunk Optional final chunk of data to send
927
1350
  */
928
1351
  end(finalChunk?: unknown) {
929
- if (this.#closed) {
1352
+ if (this._closed) {
930
1353
  throw new Error("StreamingResponse is already closed");
931
1354
  }
932
- this.#closed = true;
1355
+ this._closed = true;
933
1356
  const response: RPCResponse = {
934
- type: "rpc",
935
- id: this.#id,
936
- success: true,
937
- result: finalChunk,
938
1357
  done: true,
1358
+ id: this._id,
1359
+ result: finalChunk,
1360
+ success: true,
1361
+ type: "rpc",
939
1362
  };
940
- this.#connection.send(JSON.stringify(response));
1363
+ this._connection.send(JSON.stringify(response));
941
1364
  }
942
1365
  }