agents 0.0.0-90db5ba → 0.0.0-931d633

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 (47) hide show
  1. package/dist/ai-chat-agent.d.ts +47 -4
  2. package/dist/ai-chat-agent.js +167 -67
  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 +48 -37
  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 -126
  18. package/dist/client.js.map +1 -1
  19. package/dist/index-CITGJflw.d.ts +486 -0
  20. package/dist/index.d.ts +29 -300
  21. package/dist/index.js +8 -6
  22. package/dist/mcp/client.d.ts +313 -21
  23. package/dist/mcp/client.js +3 -402
  24. package/dist/mcp/client.js.map +1 -1
  25. package/dist/mcp/do-oauth-client-provider.d.ts +3 -3
  26. package/dist/mcp/do-oauth-client-provider.js +3 -103
  27. package/dist/mcp/do-oauth-client-provider.js.map +1 -1
  28. package/dist/mcp/index.d.ts +32 -8
  29. package/dist/mcp/index.js +242 -214
  30. package/dist/mcp/index.js.map +1 -1
  31. package/dist/observability/index.d.ts +12 -0
  32. package/dist/observability/index.js +10 -0
  33. package/dist/react.d.ts +85 -5
  34. package/dist/react.js +20 -8
  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 +75 -57
  43. package/src/index.ts +542 -89
  44. package/dist/chunk-HMLY7DHA.js +0 -16
  45. package/dist/chunk-XG52S6YY.js +0 -591
  46. package/dist/chunk-XG52S6YY.js.map +0 -1
  47. /package/dist/{chunk-HMLY7DHA.js.map → observability/index.js.map} +0 -0
package/src/index.ts CHANGED
@@ -1,19 +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";
28
+ export type { Connection, ConnectionContext, WSMessage } from "partyserver";
17
29
 
18
30
  /**
19
31
  * RPC request message from client
@@ -97,7 +109,6 @@ export type CallableMetadata = {
97
109
  streaming?: boolean;
98
110
  };
99
111
 
100
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
101
112
  const callableMetadata = new Map<Function, CallableMetadata>();
102
113
 
103
114
  /**
@@ -107,6 +118,7 @@ const callableMetadata = new Map<Function, CallableMetadata>();
107
118
  export function unstable_callable(metadata: CallableMetadata = {}) {
108
119
  return function callableDecorator<This, Args extends unknown[], Return>(
109
120
  target: (this: This, ...args: Args) => Return,
121
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: later
110
122
  context: ClassMethodDecoratorContext
111
123
  ) {
112
124
  if (!callableMetadata.has(target)) {
@@ -158,24 +170,95 @@ function getNextCronTime(cron: string) {
158
170
  return interval.getNextDate();
159
171
  }
160
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
+
161
215
  const STATE_ROW_ID = "cf_state_row_id";
162
216
  const STATE_WAS_CHANGED = "cf_state_was_changed";
163
217
 
164
218
  const DEFAULT_STATE = {} as unknown;
165
219
 
166
- export const unstable_context = new AsyncLocalStorage<{
220
+ const agentContext = new AsyncLocalStorage<{
167
221
  agent: Agent<unknown>;
168
222
  connection: Connection | undefined;
169
223
  request: Request | undefined;
170
224
  }>();
171
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
+
172
250
  /**
173
251
  * Base class for creating Agent implementations
174
252
  * @template Env Environment type containing bindings
175
253
  * @template State State type to store within the Agent
176
254
  */
177
255
  export class Agent<Env, State = unknown> extends Server<Env> {
178
- #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");
179
262
 
180
263
  /**
181
264
  * Initial state for the Agent
@@ -187,9 +270,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
187
270
  * Current state of the Agent
188
271
  */
189
272
  get state(): State {
190
- if (this.#state !== DEFAULT_STATE) {
273
+ if (this._state !== DEFAULT_STATE) {
191
274
  // state was previously set, and populated internal state
192
- return this.#state;
275
+ return this._state;
193
276
  }
194
277
  // looks like this is the first time the state is being accessed
195
278
  // check if the state was set in a previous life
@@ -209,8 +292,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
209
292
  ) {
210
293
  const state = result[0]?.state as string; // could be null?
211
294
 
212
- this.#state = JSON.parse(state);
213
- return this.#state;
295
+ this._state = JSON.parse(state);
296
+ return this._state;
214
297
  }
215
298
 
216
299
  // ok, this is the first time the state is being accessed
@@ -234,6 +317,11 @@ export class Agent<Env, State = unknown> extends Server<Env> {
234
317
  hibernate: true, // default to hibernate
235
318
  };
236
319
 
320
+ /**
321
+ * The observability implementation to use for the Agent
322
+ */
323
+ observability?: Observability = genericObservability;
324
+
237
325
  /**
238
326
  * Execute SQL queries against the Agent's database
239
327
  * @template T Type of the returned rows
@@ -271,7 +359,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
271
359
  `;
272
360
 
273
361
  void this.ctx.blockConcurrencyWhile(async () => {
274
- return this.#tryCatch(async () => {
362
+ return this._tryCatch(async () => {
275
363
  // Create alarms table if it doesn't exist
276
364
  this.sql`
277
365
  CREATE TABLE IF NOT EXISTS cf_agents_schedules (
@@ -291,25 +379,65 @@ export class Agent<Env, State = unknown> extends Server<Env> {
291
379
  });
292
380
  });
293
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
+
294
422
  const _onMessage = this.onMessage.bind(this);
295
423
  this.onMessage = async (connection: Connection, message: WSMessage) => {
296
- return unstable_context.run(
424
+ return agentContext.run(
297
425
  { agent: this, connection, request: undefined },
298
426
  async () => {
299
427
  if (typeof message !== "string") {
300
- return this.#tryCatch(() => _onMessage(connection, message));
428
+ return this._tryCatch(() => _onMessage(connection, message));
301
429
  }
302
430
 
303
431
  let parsed: unknown;
304
432
  try {
305
433
  parsed = JSON.parse(message);
306
- } catch (e) {
434
+ } catch (_e) {
307
435
  // silently fail and let the onMessage handler handle it
308
- return this.#tryCatch(() => _onMessage(connection, message));
436
+ return this._tryCatch(() => _onMessage(connection, message));
309
437
  }
310
438
 
311
439
  if (isStateUpdateMessage(parsed)) {
312
- this.#setStateInternal(parsed.state as State, connection);
440
+ this._setStateInternal(parsed.state as State, connection);
313
441
  return;
314
442
  }
315
443
 
@@ -323,11 +451,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
323
451
  throw new Error(`Method ${method} does not exist`);
324
452
  }
325
453
 
326
- if (!this.#isCallable(method)) {
454
+ if (!this._isCallable(method)) {
327
455
  throw new Error(`Method ${method} is not callable`);
328
456
  }
329
457
 
330
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
331
458
  const metadata = callableMetadata.get(methodFn as Function);
332
459
 
333
460
  // For streaming methods, pass a StreamingResponse object
@@ -339,22 +466,39 @@ export class Agent<Env, State = unknown> extends Server<Env> {
339
466
 
340
467
  // For regular methods, execute and send response
341
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
+
342
486
  const response: RPCResponse = {
343
- type: "rpc",
487
+ done: true,
344
488
  id,
345
- success: true,
346
489
  result,
347
- done: true,
490
+ success: true,
491
+ type: "rpc",
348
492
  };
349
493
  connection.send(JSON.stringify(response));
350
494
  } catch (e) {
351
495
  // Send error response
352
496
  const response: RPCResponse = {
353
- type: "rpc",
354
- id: parsed.id,
355
- success: false,
356
497
  error:
357
498
  e instanceof Error ? e.message : "Unknown error occurred",
499
+ id: parsed.id,
500
+ success: false,
501
+ type: "rpc",
358
502
  };
359
503
  connection.send(JSON.stringify(response));
360
504
  console.error("RPC error:", e);
@@ -362,7 +506,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
362
506
  return;
363
507
  }
364
508
 
365
- return this.#tryCatch(() => _onMessage(connection, message));
509
+ return this._tryCatch(() => _onMessage(connection, message));
366
510
  }
367
511
  );
368
512
  };
@@ -371,27 +515,89 @@ export class Agent<Env, State = unknown> extends Server<Env> {
371
515
  this.onConnect = (connection: Connection, ctx: ConnectionContext) => {
372
516
  // TODO: This is a hack to ensure the state is sent after the connection is established
373
517
  // must fix this
374
- return unstable_context.run(
518
+ return agentContext.run(
375
519
  { agent: this, connection, request: ctx.request },
376
520
  async () => {
377
521
  setTimeout(() => {
378
522
  if (this.state) {
379
523
  connection.send(
380
524
  JSON.stringify({
381
- type: "cf_agent_state",
382
525
  state: this.state,
526
+ type: "cf_agent_state",
383
527
  })
384
528
  );
385
529
  }
386
- 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));
387
551
  }, 20);
388
552
  }
389
553
  );
390
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
+ };
391
593
  }
392
594
 
393
- #setStateInternal(state: State, source: Connection | "server" = "server") {
394
- this.#state = state;
595
+ private _setStateInternal(
596
+ state: State,
597
+ source: Connection | "server" = "server"
598
+ ) {
599
+ const previousState = this._state;
600
+ this._state = state;
395
601
  this.sql`
396
602
  INSERT OR REPLACE INTO cf_agents_state (id, state)
397
603
  VALUES (${STATE_ROW_ID}, ${JSON.stringify(state)})
@@ -402,16 +608,29 @@ export class Agent<Env, State = unknown> extends Server<Env> {
402
608
  `;
403
609
  this.broadcast(
404
610
  JSON.stringify({
405
- type: "cf_agent_state",
406
611
  state: state,
612
+ type: "cf_agent_state",
407
613
  }),
408
614
  source !== "server" ? [source.id] : []
409
615
  );
410
- return this.#tryCatch(() => {
411
- const { connection, request } = unstable_context.getStore() || {};
412
- return unstable_context.run(
616
+ return this._tryCatch(() => {
617
+ const { connection, request } = agentContext.getStore() || {};
618
+ return agentContext.run(
413
619
  { agent: this, connection, request },
414
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
+ );
415
634
  return this.onStateUpdate(state, source);
416
635
  }
417
636
  );
@@ -423,7 +642,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
423
642
  * @param state New state to set
424
643
  */
425
644
  setState(state: State) {
426
- this.#setStateInternal(state, "server");
645
+ this._setStateInternal(state, "server");
427
646
  }
428
647
 
429
648
  /**
@@ -431,6 +650,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
431
650
  * @param state Updated state
432
651
  * @param source Source of the state update ("server" or a client connection)
433
652
  */
653
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
434
654
  onStateUpdate(state: State | undefined, source: Connection | "server") {
435
655
  // override this to handle state updates
436
656
  }
@@ -439,8 +659,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
439
659
  * Called when the Agent receives an email
440
660
  * @param email Email message to process
441
661
  */
662
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
442
663
  onEmail(email: ForwardableEmailMessage) {
443
- return unstable_context.run(
664
+ return agentContext.run(
444
665
  { agent: this, connection: undefined, request: undefined },
445
666
  async () => {
446
667
  console.error("onEmail not implemented");
@@ -448,7 +669,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
448
669
  );
449
670
  }
450
671
 
451
- async #tryCatch<T>(fn: () => T | Promise<T>) {
672
+ private async _tryCatch<T>(fn: () => T | Promise<T>) {
452
673
  try {
453
674
  return await fn();
454
675
  } catch (e) {
@@ -505,6 +726,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
505
726
  ): Promise<Schedule<T>> {
506
727
  const id = nanoid(9);
507
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
+
508
741
  if (typeof callback !== "string") {
509
742
  throw new Error("Callback must be a string");
510
743
  }
@@ -522,15 +755,19 @@ export class Agent<Env, State = unknown> extends Server<Env> {
522
755
  )}, 'scheduled', ${timestamp})
523
756
  `;
524
757
 
525
- await this.#scheduleNextAlarm();
758
+ await this._scheduleNextAlarm();
526
759
 
527
- return {
528
- id,
760
+ const schedule: Schedule<T> = {
529
761
  callback: callback,
762
+ id,
530
763
  payload: payload as T,
531
764
  time: timestamp,
532
765
  type: "scheduled",
533
766
  };
767
+
768
+ emitScheduleCreate(schedule);
769
+
770
+ return schedule;
534
771
  }
535
772
  if (typeof when === "number") {
536
773
  const time = new Date(Date.now() + when * 1000);
@@ -543,16 +780,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
543
780
  )}, 'delayed', ${when}, ${timestamp})
544
781
  `;
545
782
 
546
- await this.#scheduleNextAlarm();
783
+ await this._scheduleNextAlarm();
547
784
 
548
- return {
549
- id,
785
+ const schedule: Schedule<T> = {
550
786
  callback: callback,
551
- payload: payload as T,
552
787
  delayInSeconds: when,
788
+ id,
789
+ payload: payload as T,
553
790
  time: timestamp,
554
791
  type: "delayed",
555
792
  };
793
+
794
+ emitScheduleCreate(schedule);
795
+
796
+ return schedule;
556
797
  }
557
798
  if (typeof when === "string") {
558
799
  const nextExecutionTime = getNextCronTime(when);
@@ -565,16 +806,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
565
806
  )}, 'cron', ${when}, ${timestamp})
566
807
  `;
567
808
 
568
- await this.#scheduleNextAlarm();
809
+ await this._scheduleNextAlarm();
569
810
 
570
- return {
571
- id,
811
+ const schedule: Schedule<T> = {
572
812
  callback: callback,
573
- payload: payload as T,
574
813
  cron: when,
814
+ id,
815
+ payload: payload as T,
575
816
  time: timestamp,
576
817
  type: "cron",
577
818
  };
819
+
820
+ emitScheduleCreate(schedule);
821
+
822
+ return schedule;
578
823
  }
579
824
  throw new Error("Invalid schedule type");
580
825
  }
@@ -650,13 +895,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
650
895
  * @returns true if the task was cancelled, false otherwise
651
896
  */
652
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
+ }
653
911
  this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
654
912
 
655
- await this.#scheduleNextAlarm();
913
+ await this._scheduleNextAlarm();
656
914
  return true;
657
915
  }
658
916
 
659
- async #scheduleNextAlarm() {
917
+ private async _scheduleNextAlarm() {
660
918
  // Find the next schedule that needs to be executed
661
919
  const result = this.sql`
662
920
  SELECT time FROM cf_agents_schedules
@@ -673,10 +931,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
673
931
  }
674
932
 
675
933
  /**
676
- * Method called when an alarm fires
677
- * 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/}
678
940
  */
679
- async alarm() {
941
+ public readonly alarm = async () => {
680
942
  const now = Math.floor(Date.now() / 1000);
681
943
 
682
944
  // Get all schedules that should be executed now
@@ -690,10 +952,21 @@ export class Agent<Env, State = unknown> extends Server<Env> {
690
952
  console.error(`callback ${row.callback} not found`);
691
953
  continue;
692
954
  }
693
- await unstable_context.run(
955
+ await agentContext.run(
694
956
  { agent: this, connection: undefined, request: undefined },
695
957
  async () => {
696
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
+
697
970
  await (
698
971
  callback as (
699
972
  payload: unknown,
@@ -722,8 +995,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
722
995
  }
723
996
 
724
997
  // Schedule the next alarm
725
- await this.#scheduleNextAlarm();
726
- }
998
+ await this._scheduleNextAlarm();
999
+ };
727
1000
 
728
1001
  /**
729
1002
  * Destroy the Agent, removing all state and scheduled tasks
@@ -732,20 +1005,200 @@ export class Agent<Env, State = unknown> extends Server<Env> {
732
1005
  // drop all tables
733
1006
  this.sql`DROP TABLE IF EXISTS cf_agents_state`;
734
1007
  this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
1008
+ this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
735
1009
 
736
1010
  // delete all alarms
737
1011
  await this.ctx.storage.deleteAlarm();
738
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
+ );
739
1025
  }
740
1026
 
741
1027
  /**
742
1028
  * Get all methods marked as callable on this Agent
743
1029
  * @returns A map of method names to their metadata
744
1030
  */
745
- #isCallable(method: string): boolean {
746
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
1031
+ private _isCallable(method: string): boolean {
747
1032
  return callableMetadata.has(this[method as keyof this] as Function);
748
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
+ }
749
1202
  }
750
1203
 
751
1204
  /**
@@ -785,9 +1238,9 @@ export async function routeAgentRequest<Env>(
785
1238
  const corsHeaders =
786
1239
  options?.cors === true
787
1240
  ? {
788
- "Access-Control-Allow-Origin": "*",
789
- "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
790
1241
  "Access-Control-Allow-Credentials": "true",
1242
+ "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
1243
+ "Access-Control-Allow-Origin": "*",
791
1244
  "Access-Control-Max-Age": "86400",
792
1245
  }
793
1246
  : options?.cors;
@@ -835,9 +1288,9 @@ export async function routeAgentRequest<Env>(
835
1288
  * @param options Routing options
836
1289
  */
837
1290
  export async function routeAgentEmail<Env>(
838
- email: ForwardableEmailMessage,
839
- env: Env,
840
- options?: AgentOptions<Env>
1291
+ _email: ForwardableEmailMessage,
1292
+ _env: Env,
1293
+ _options?: AgentOptions<Env>
841
1294
  ): Promise<void> {}
842
1295
 
843
1296
  /**
@@ -849,7 +1302,7 @@ export async function routeAgentEmail<Env>(
849
1302
  * @param options Options for Agent creation
850
1303
  * @returns Promise resolving to an Agent instance stub
851
1304
  */
852
- export function getAgentByName<Env, T extends Agent<Env>>(
1305
+ export async function getAgentByName<Env, T extends Agent<Env>>(
853
1306
  namespace: AgentNamespace<T>,
854
1307
  name: string,
855
1308
  options?: {
@@ -864,13 +1317,13 @@ export function getAgentByName<Env, T extends Agent<Env>>(
864
1317
  * A wrapper for streaming responses in callable methods
865
1318
  */
866
1319
  export class StreamingResponse {
867
- #connection: Connection;
868
- #id: string;
869
- #closed = false;
1320
+ private _connection: Connection;
1321
+ private _id: string;
1322
+ private _closed = false;
870
1323
 
871
1324
  constructor(connection: Connection, id: string) {
872
- this.#connection = connection;
873
- this.#id = id;
1325
+ this._connection = connection;
1326
+ this._id = id;
874
1327
  }
875
1328
 
876
1329
  /**
@@ -878,17 +1331,17 @@ export class StreamingResponse {
878
1331
  * @param chunk The data to send
879
1332
  */
880
1333
  send(chunk: unknown) {
881
- if (this.#closed) {
1334
+ if (this._closed) {
882
1335
  throw new Error("StreamingResponse is already closed");
883
1336
  }
884
1337
  const response: RPCResponse = {
885
- type: "rpc",
886
- id: this.#id,
887
- success: true,
888
- result: chunk,
889
1338
  done: false,
1339
+ id: this._id,
1340
+ result: chunk,
1341
+ success: true,
1342
+ type: "rpc",
890
1343
  };
891
- this.#connection.send(JSON.stringify(response));
1344
+ this._connection.send(JSON.stringify(response));
892
1345
  }
893
1346
 
894
1347
  /**
@@ -896,17 +1349,17 @@ export class StreamingResponse {
896
1349
  * @param finalChunk Optional final chunk of data to send
897
1350
  */
898
1351
  end(finalChunk?: unknown) {
899
- if (this.#closed) {
1352
+ if (this._closed) {
900
1353
  throw new Error("StreamingResponse is already closed");
901
1354
  }
902
- this.#closed = true;
1355
+ this._closed = true;
903
1356
  const response: RPCResponse = {
904
- type: "rpc",
905
- id: this.#id,
906
- success: true,
907
- result: finalChunk,
908
1357
  done: true,
1358
+ id: this._id,
1359
+ result: finalChunk,
1360
+ success: true,
1361
+ type: "rpc",
909
1362
  };
910
- this.#connection.send(JSON.stringify(response));
1363
+ this._connection.send(JSON.stringify(response));
911
1364
  }
912
1365
  }