agents 0.0.0-db5b372 → 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 (46) hide show
  1. package/dist/ai-chat-agent.d.ts +32 -6
  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 +29 -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-YZNSS675.js → chunk-E3LCYPCB.js} +71 -37
  10. package/dist/chunk-E3LCYPCB.js.map +1 -0
  11. package/dist/chunk-JFRK72K3.js +910 -0
  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 -307
  20. package/dist/index.js +8 -7
  21. package/dist/mcp/client.d.ts +310 -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 +22 -11
  27. package/dist/mcp/index.js +172 -175
  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 +538 -91
  42. package/dist/chunk-AV3OMRR4.js +0 -597
  43. package/dist/chunk-AV3OMRR4.js.map +0 -1
  44. package/dist/chunk-HMLY7DHA.js +0 -16
  45. package/dist/chunk-YZNSS675.js.map +0 -1
  46. /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,29 +170,95 @@ 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
 
165
218
  const DEFAULT_STATE = {} as unknown;
166
219
 
167
- export const unstable_context = new AsyncLocalStorage<{
220
+ const agentContext = new AsyncLocalStorage<{
168
221
  agent: Agent<unknown>;
169
222
  connection: Connection | undefined;
170
223
  request: Request | undefined;
171
224
  }>();
172
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
+
173
250
  /**
174
251
  * Base class for creating Agent implementations
175
252
  * @template Env Environment type containing bindings
176
253
  * @template State State type to store within the Agent
177
254
  */
178
255
  export class Agent<Env, State = unknown> extends Server<Env> {
179
- #state = DEFAULT_STATE as State;
256
+ private _state = DEFAULT_STATE as State;
180
257
 
181
- #ParentClass: typeof Agent<Env, State> =
258
+ private _ParentClass: typeof Agent<Env, State> =
182
259
  Object.getPrototypeOf(this).constructor;
183
260
 
184
- mcp: MCPClientManager = new MCPClientManager(this.#ParentClass.name, "0.0.1");
261
+ mcp: MCPClientManager = new MCPClientManager(this._ParentClass.name, "0.0.1");
185
262
 
186
263
  /**
187
264
  * Initial state for the Agent
@@ -193,9 +270,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
193
270
  * Current state of the Agent
194
271
  */
195
272
  get state(): State {
196
- if (this.#state !== DEFAULT_STATE) {
273
+ if (this._state !== DEFAULT_STATE) {
197
274
  // state was previously set, and populated internal state
198
- return this.#state;
275
+ return this._state;
199
276
  }
200
277
  // looks like this is the first time the state is being accessed
201
278
  // check if the state was set in a previous life
@@ -215,8 +292,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
215
292
  ) {
216
293
  const state = result[0]?.state as string; // could be null?
217
294
 
218
- this.#state = JSON.parse(state);
219
- return this.#state;
295
+ this._state = JSON.parse(state);
296
+ return this._state;
220
297
  }
221
298
 
222
299
  // ok, this is the first time the state is being accessed
@@ -240,6 +317,11 @@ export class Agent<Env, State = unknown> extends Server<Env> {
240
317
  hibernate: true, // default to hibernate
241
318
  };
242
319
 
320
+ /**
321
+ * The observability implementation to use for the Agent
322
+ */
323
+ observability?: Observability = genericObservability;
324
+
243
325
  /**
244
326
  * Execute SQL queries against the Agent's database
245
327
  * @template T Type of the returned rows
@@ -277,7 +359,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
277
359
  `;
278
360
 
279
361
  void this.ctx.blockConcurrencyWhile(async () => {
280
- return this.#tryCatch(async () => {
362
+ return this._tryCatch(async () => {
281
363
  // Create alarms table if it doesn't exist
282
364
  this.sql`
283
365
  CREATE TABLE IF NOT EXISTS cf_agents_schedules (
@@ -297,25 +379,65 @@ export class Agent<Env, State = unknown> extends Server<Env> {
297
379
  });
298
380
  });
299
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
+
300
422
  const _onMessage = this.onMessage.bind(this);
301
423
  this.onMessage = async (connection: Connection, message: WSMessage) => {
302
- return unstable_context.run(
424
+ return agentContext.run(
303
425
  { agent: this, connection, request: undefined },
304
426
  async () => {
305
427
  if (typeof message !== "string") {
306
- return this.#tryCatch(() => _onMessage(connection, message));
428
+ return this._tryCatch(() => _onMessage(connection, message));
307
429
  }
308
430
 
309
431
  let parsed: unknown;
310
432
  try {
311
433
  parsed = JSON.parse(message);
312
- } catch (e) {
434
+ } catch (_e) {
313
435
  // silently fail and let the onMessage handler handle it
314
- return this.#tryCatch(() => _onMessage(connection, message));
436
+ return this._tryCatch(() => _onMessage(connection, message));
315
437
  }
316
438
 
317
439
  if (isStateUpdateMessage(parsed)) {
318
- this.#setStateInternal(parsed.state as State, connection);
440
+ this._setStateInternal(parsed.state as State, connection);
319
441
  return;
320
442
  }
321
443
 
@@ -329,11 +451,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
329
451
  throw new Error(`Method ${method} does not exist`);
330
452
  }
331
453
 
332
- if (!this.#isCallable(method)) {
454
+ if (!this._isCallable(method)) {
333
455
  throw new Error(`Method ${method} is not callable`);
334
456
  }
335
457
 
336
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
337
458
  const metadata = callableMetadata.get(methodFn as Function);
338
459
 
339
460
  // For streaming methods, pass a StreamingResponse object
@@ -345,22 +466,39 @@ export class Agent<Env, State = unknown> extends Server<Env> {
345
466
 
346
467
  // For regular methods, execute and send response
347
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
+
348
486
  const response: RPCResponse = {
349
- type: "rpc",
487
+ done: true,
350
488
  id,
351
- success: true,
352
489
  result,
353
- done: true,
490
+ success: true,
491
+ type: "rpc",
354
492
  };
355
493
  connection.send(JSON.stringify(response));
356
494
  } catch (e) {
357
495
  // Send error response
358
496
  const response: RPCResponse = {
359
- type: "rpc",
360
- id: parsed.id,
361
- success: false,
362
497
  error:
363
498
  e instanceof Error ? e.message : "Unknown error occurred",
499
+ id: parsed.id,
500
+ success: false,
501
+ type: "rpc",
364
502
  };
365
503
  connection.send(JSON.stringify(response));
366
504
  console.error("RPC error:", e);
@@ -368,7 +506,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
368
506
  return;
369
507
  }
370
508
 
371
- return this.#tryCatch(() => _onMessage(connection, message));
509
+ return this._tryCatch(() => _onMessage(connection, message));
372
510
  }
373
511
  );
374
512
  };
@@ -377,27 +515,89 @@ export class Agent<Env, State = unknown> extends Server<Env> {
377
515
  this.onConnect = (connection: Connection, ctx: ConnectionContext) => {
378
516
  // TODO: This is a hack to ensure the state is sent after the connection is established
379
517
  // must fix this
380
- return unstable_context.run(
518
+ return agentContext.run(
381
519
  { agent: this, connection, request: ctx.request },
382
520
  async () => {
383
521
  setTimeout(() => {
384
522
  if (this.state) {
385
523
  connection.send(
386
524
  JSON.stringify({
387
- type: "cf_agent_state",
388
525
  state: this.state,
526
+ type: "cf_agent_state",
389
527
  })
390
528
  );
391
529
  }
392
- 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));
393
551
  }, 20);
394
552
  }
395
553
  );
396
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
+ };
397
593
  }
398
594
 
399
- #setStateInternal(state: State, source: Connection | "server" = "server") {
400
- this.#state = state;
595
+ private _setStateInternal(
596
+ state: State,
597
+ source: Connection | "server" = "server"
598
+ ) {
599
+ const previousState = this._state;
600
+ this._state = state;
401
601
  this.sql`
402
602
  INSERT OR REPLACE INTO cf_agents_state (id, state)
403
603
  VALUES (${STATE_ROW_ID}, ${JSON.stringify(state)})
@@ -408,16 +608,29 @@ export class Agent<Env, State = unknown> extends Server<Env> {
408
608
  `;
409
609
  this.broadcast(
410
610
  JSON.stringify({
411
- type: "cf_agent_state",
412
611
  state: state,
612
+ type: "cf_agent_state",
413
613
  }),
414
614
  source !== "server" ? [source.id] : []
415
615
  );
416
- return this.#tryCatch(() => {
417
- const { connection, request } = unstable_context.getStore() || {};
418
- return unstable_context.run(
616
+ return this._tryCatch(() => {
617
+ const { connection, request } = agentContext.getStore() || {};
618
+ return agentContext.run(
419
619
  { agent: this, connection, request },
420
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
+ );
421
634
  return this.onStateUpdate(state, source);
422
635
  }
423
636
  );
@@ -429,7 +642,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
429
642
  * @param state New state to set
430
643
  */
431
644
  setState(state: State) {
432
- this.#setStateInternal(state, "server");
645
+ this._setStateInternal(state, "server");
433
646
  }
434
647
 
435
648
  /**
@@ -437,6 +650,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
437
650
  * @param state Updated state
438
651
  * @param source Source of the state update ("server" or a client connection)
439
652
  */
653
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
440
654
  onStateUpdate(state: State | undefined, source: Connection | "server") {
441
655
  // override this to handle state updates
442
656
  }
@@ -445,8 +659,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
445
659
  * Called when the Agent receives an email
446
660
  * @param email Email message to process
447
661
  */
662
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
448
663
  onEmail(email: ForwardableEmailMessage) {
449
- return unstable_context.run(
664
+ return agentContext.run(
450
665
  { agent: this, connection: undefined, request: undefined },
451
666
  async () => {
452
667
  console.error("onEmail not implemented");
@@ -454,7 +669,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
454
669
  );
455
670
  }
456
671
 
457
- async #tryCatch<T>(fn: () => T | Promise<T>) {
672
+ private async _tryCatch<T>(fn: () => T | Promise<T>) {
458
673
  try {
459
674
  return await fn();
460
675
  } catch (e) {
@@ -511,6 +726,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
511
726
  ): Promise<Schedule<T>> {
512
727
  const id = nanoid(9);
513
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
+
514
741
  if (typeof callback !== "string") {
515
742
  throw new Error("Callback must be a string");
516
743
  }
@@ -528,15 +755,19 @@ export class Agent<Env, State = unknown> extends Server<Env> {
528
755
  )}, 'scheduled', ${timestamp})
529
756
  `;
530
757
 
531
- await this.#scheduleNextAlarm();
758
+ await this._scheduleNextAlarm();
532
759
 
533
- return {
534
- id,
760
+ const schedule: Schedule<T> = {
535
761
  callback: callback,
762
+ id,
536
763
  payload: payload as T,
537
764
  time: timestamp,
538
765
  type: "scheduled",
539
766
  };
767
+
768
+ emitScheduleCreate(schedule);
769
+
770
+ return schedule;
540
771
  }
541
772
  if (typeof when === "number") {
542
773
  const time = new Date(Date.now() + when * 1000);
@@ -549,16 +780,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
549
780
  )}, 'delayed', ${when}, ${timestamp})
550
781
  `;
551
782
 
552
- await this.#scheduleNextAlarm();
783
+ await this._scheduleNextAlarm();
553
784
 
554
- return {
555
- id,
785
+ const schedule: Schedule<T> = {
556
786
  callback: callback,
557
- payload: payload as T,
558
787
  delayInSeconds: when,
788
+ id,
789
+ payload: payload as T,
559
790
  time: timestamp,
560
791
  type: "delayed",
561
792
  };
793
+
794
+ emitScheduleCreate(schedule);
795
+
796
+ return schedule;
562
797
  }
563
798
  if (typeof when === "string") {
564
799
  const nextExecutionTime = getNextCronTime(when);
@@ -571,16 +806,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
571
806
  )}, 'cron', ${when}, ${timestamp})
572
807
  `;
573
808
 
574
- await this.#scheduleNextAlarm();
809
+ await this._scheduleNextAlarm();
575
810
 
576
- return {
577
- id,
811
+ const schedule: Schedule<T> = {
578
812
  callback: callback,
579
- payload: payload as T,
580
813
  cron: when,
814
+ id,
815
+ payload: payload as T,
581
816
  time: timestamp,
582
817
  type: "cron",
583
818
  };
819
+
820
+ emitScheduleCreate(schedule);
821
+
822
+ return schedule;
584
823
  }
585
824
  throw new Error("Invalid schedule type");
586
825
  }
@@ -656,13 +895,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
656
895
  * @returns true if the task was cancelled, false otherwise
657
896
  */
658
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
+ }
659
911
  this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
660
912
 
661
- await this.#scheduleNextAlarm();
913
+ await this._scheduleNextAlarm();
662
914
  return true;
663
915
  }
664
916
 
665
- async #scheduleNextAlarm() {
917
+ private async _scheduleNextAlarm() {
666
918
  // Find the next schedule that needs to be executed
667
919
  const result = this.sql`
668
920
  SELECT time FROM cf_agents_schedules
@@ -679,10 +931,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
679
931
  }
680
932
 
681
933
  /**
682
- * Method called when an alarm fires
683
- * 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/}
684
940
  */
685
- async alarm() {
941
+ public readonly alarm = async () => {
686
942
  const now = Math.floor(Date.now() / 1000);
687
943
 
688
944
  // Get all schedules that should be executed now
@@ -696,10 +952,21 @@ export class Agent<Env, State = unknown> extends Server<Env> {
696
952
  console.error(`callback ${row.callback} not found`);
697
953
  continue;
698
954
  }
699
- await unstable_context.run(
955
+ await agentContext.run(
700
956
  { agent: this, connection: undefined, request: undefined },
701
957
  async () => {
702
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
+
703
970
  await (
704
971
  callback as (
705
972
  payload: unknown,
@@ -728,8 +995,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
728
995
  }
729
996
 
730
997
  // Schedule the next alarm
731
- await this.#scheduleNextAlarm();
732
- }
998
+ await this._scheduleNextAlarm();
999
+ };
733
1000
 
734
1001
  /**
735
1002
  * Destroy the Agent, removing all state and scheduled tasks
@@ -738,20 +1005,200 @@ export class Agent<Env, State = unknown> extends Server<Env> {
738
1005
  // drop all tables
739
1006
  this.sql`DROP TABLE IF EXISTS cf_agents_state`;
740
1007
  this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
1008
+ this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
741
1009
 
742
1010
  // delete all alarms
743
1011
  await this.ctx.storage.deleteAlarm();
744
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
+ );
745
1025
  }
746
1026
 
747
1027
  /**
748
1028
  * Get all methods marked as callable on this Agent
749
1029
  * @returns A map of method names to their metadata
750
1030
  */
751
- #isCallable(method: string): boolean {
752
- // biome-ignore lint/complexity/noBannedTypes: <explanation>
1031
+ private _isCallable(method: string): boolean {
753
1032
  return callableMetadata.has(this[method as keyof this] as Function);
754
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
+ }
755
1202
  }
756
1203
 
757
1204
  /**
@@ -791,9 +1238,9 @@ export async function routeAgentRequest<Env>(
791
1238
  const corsHeaders =
792
1239
  options?.cors === true
793
1240
  ? {
794
- "Access-Control-Allow-Origin": "*",
795
- "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
796
1241
  "Access-Control-Allow-Credentials": "true",
1242
+ "Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
1243
+ "Access-Control-Allow-Origin": "*",
797
1244
  "Access-Control-Max-Age": "86400",
798
1245
  }
799
1246
  : options?.cors;
@@ -841,9 +1288,9 @@ export async function routeAgentRequest<Env>(
841
1288
  * @param options Routing options
842
1289
  */
843
1290
  export async function routeAgentEmail<Env>(
844
- email: ForwardableEmailMessage,
845
- env: Env,
846
- options?: AgentOptions<Env>
1291
+ _email: ForwardableEmailMessage,
1292
+ _env: Env,
1293
+ _options?: AgentOptions<Env>
847
1294
  ): Promise<void> {}
848
1295
 
849
1296
  /**
@@ -855,7 +1302,7 @@ export async function routeAgentEmail<Env>(
855
1302
  * @param options Options for Agent creation
856
1303
  * @returns Promise resolving to an Agent instance stub
857
1304
  */
858
- export function getAgentByName<Env, T extends Agent<Env>>(
1305
+ export async function getAgentByName<Env, T extends Agent<Env>>(
859
1306
  namespace: AgentNamespace<T>,
860
1307
  name: string,
861
1308
  options?: {
@@ -870,13 +1317,13 @@ export function getAgentByName<Env, T extends Agent<Env>>(
870
1317
  * A wrapper for streaming responses in callable methods
871
1318
  */
872
1319
  export class StreamingResponse {
873
- #connection: Connection;
874
- #id: string;
875
- #closed = false;
1320
+ private _connection: Connection;
1321
+ private _id: string;
1322
+ private _closed = false;
876
1323
 
877
1324
  constructor(connection: Connection, id: string) {
878
- this.#connection = connection;
879
- this.#id = id;
1325
+ this._connection = connection;
1326
+ this._id = id;
880
1327
  }
881
1328
 
882
1329
  /**
@@ -884,17 +1331,17 @@ export class StreamingResponse {
884
1331
  * @param chunk The data to send
885
1332
  */
886
1333
  send(chunk: unknown) {
887
- if (this.#closed) {
1334
+ if (this._closed) {
888
1335
  throw new Error("StreamingResponse is already closed");
889
1336
  }
890
1337
  const response: RPCResponse = {
891
- type: "rpc",
892
- id: this.#id,
893
- success: true,
894
- result: chunk,
895
1338
  done: false,
1339
+ id: this._id,
1340
+ result: chunk,
1341
+ success: true,
1342
+ type: "rpc",
896
1343
  };
897
- this.#connection.send(JSON.stringify(response));
1344
+ this._connection.send(JSON.stringify(response));
898
1345
  }
899
1346
 
900
1347
  /**
@@ -902,17 +1349,17 @@ export class StreamingResponse {
902
1349
  * @param finalChunk Optional final chunk of data to send
903
1350
  */
904
1351
  end(finalChunk?: unknown) {
905
- if (this.#closed) {
1352
+ if (this._closed) {
906
1353
  throw new Error("StreamingResponse is already closed");
907
1354
  }
908
- this.#closed = true;
1355
+ this._closed = true;
909
1356
  const response: RPCResponse = {
910
- type: "rpc",
911
- id: this.#id,
912
- success: true,
913
- result: finalChunk,
914
1357
  done: true,
1358
+ id: this._id,
1359
+ result: finalChunk,
1360
+ success: true,
1361
+ type: "rpc",
915
1362
  };
916
- this.#connection.send(JSON.stringify(response));
1363
+ this._connection.send(JSON.stringify(response));
917
1364
  }
918
1365
  }