agents 0.0.0-07086ea → 0.0.0-0bb74b8
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.
- package/README.md +22 -22
- package/dist/ai-chat-agent.d.ts +32 -6
- package/dist/ai-chat-agent.js +149 -115
- package/dist/ai-chat-agent.js.map +1 -1
- package/dist/ai-react.d.ts +18 -5
- package/dist/ai-react.js +28 -29
- package/dist/ai-react.js.map +1 -1
- package/dist/chunk-KUH345EY.js +116 -0
- package/dist/chunk-KUH345EY.js.map +1 -0
- package/dist/chunk-MGHXAF5T.js +1082 -0
- package/dist/chunk-MGHXAF5T.js.map +1 -0
- package/dist/{chunk-WNICV3OI.js → chunk-MW5BQ2FW.js} +70 -37
- package/dist/chunk-MW5BQ2FW.js.map +1 -0
- package/dist/chunk-PVQZBKN7.js +106 -0
- package/dist/chunk-PVQZBKN7.js.map +1 -0
- package/dist/client.d.ts +16 -2
- package/dist/client.js +6 -126
- package/dist/client.js.map +1 -1
- package/dist/index-CEVsIbwa.d.ts +565 -0
- package/dist/index.d.ts +32 -307
- package/dist/index.js +14 -7
- package/dist/mcp/client.d.ts +310 -23
- package/dist/mcp/client.js +1 -2
- package/dist/mcp/do-oauth-client-provider.d.ts +3 -3
- package/dist/mcp/do-oauth-client-provider.js +3 -103
- package/dist/mcp/do-oauth-client-provider.js.map +1 -1
- package/dist/mcp/index.d.ts +23 -13
- package/dist/mcp/index.js +150 -175
- package/dist/mcp/index.js.map +1 -1
- package/dist/observability/index.d.ts +12 -0
- package/dist/observability/index.js +10 -0
- package/dist/react.d.ts +85 -5
- package/dist/react.js +20 -8
- package/dist/react.js.map +1 -1
- package/dist/schedule.d.ts +6 -6
- package/dist/schedule.js +4 -6
- package/dist/schedule.js.map +1 -1
- package/dist/serializable.d.ts +32 -0
- package/dist/serializable.js +1 -0
- package/dist/serializable.js.map +1 -0
- package/package.json +76 -68
- package/src/index.ts +828 -111
- package/dist/chunk-HMLY7DHA.js +0 -16
- package/dist/chunk-WNICV3OI.js.map +0 -1
- package/dist/chunk-ZRZEISHY.js +0 -597
- package/dist/chunk-ZRZEISHY.js.map +0 -1
- /package/dist/{chunk-HMLY7DHA.js.map → observability/index.js.map} +0 -0
package/src/index.ts
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
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";
|
|
13
|
+
import { EmailMessage } from "cloudflare:email";
|
|
1
14
|
import {
|
|
2
|
-
Server,
|
|
3
|
-
routePartykitRequest,
|
|
4
|
-
type PartyServerOptions,
|
|
5
|
-
getServerByName,
|
|
6
15
|
type Connection,
|
|
7
16
|
type ConnectionContext,
|
|
17
|
+
type PartyServerOptions,
|
|
18
|
+
Server,
|
|
8
19
|
type WSMessage,
|
|
20
|
+
getServerByName,
|
|
21
|
+
routePartykitRequest
|
|
9
22
|
} from "partyserver";
|
|
10
|
-
|
|
11
|
-
import { parseCronExpression } from "cron-schedule";
|
|
12
|
-
import { nanoid } from "nanoid";
|
|
13
|
-
|
|
14
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
23
|
+
import { camelCaseToKebabCase } from "./client";
|
|
15
24
|
import { MCPClientManager } from "./mcp/client";
|
|
25
|
+
// import type { MCPClientConnection } from "./mcp/client-connection";
|
|
26
|
+
import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider";
|
|
27
|
+
import { genericObservability, type Observability } from "./observability";
|
|
16
28
|
|
|
17
|
-
export type { Connection,
|
|
29
|
+
export type { Connection, ConnectionContext, WSMessage } from "partyserver";
|
|
18
30
|
|
|
19
31
|
/**
|
|
20
32
|
* RPC request message from client
|
|
@@ -98,7 +110,6 @@ export type CallableMetadata = {
|
|
|
98
110
|
streaming?: boolean;
|
|
99
111
|
};
|
|
100
112
|
|
|
101
|
-
// biome-ignore lint/complexity/noBannedTypes: <explanation>
|
|
102
113
|
const callableMetadata = new Map<Function, CallableMetadata>();
|
|
103
114
|
|
|
104
115
|
/**
|
|
@@ -108,6 +119,7 @@ const callableMetadata = new Map<Function, CallableMetadata>();
|
|
|
108
119
|
export function unstable_callable(metadata: CallableMetadata = {}) {
|
|
109
120
|
return function callableDecorator<This, Args extends unknown[], Return>(
|
|
110
121
|
target: (this: This, ...args: Args) => Return,
|
|
122
|
+
// biome-ignore lint/correctness/noUnusedFunctionParameters: later
|
|
111
123
|
context: ClassMethodDecoratorContext
|
|
112
124
|
) {
|
|
113
125
|
if (!callableMetadata.has(target)) {
|
|
@@ -159,29 +171,96 @@ function getNextCronTime(cron: string) {
|
|
|
159
171
|
return interval.getNextDate();
|
|
160
172
|
}
|
|
161
173
|
|
|
174
|
+
/**
|
|
175
|
+
* MCP Server state update message from server -> Client
|
|
176
|
+
*/
|
|
177
|
+
export type MCPServerMessage = {
|
|
178
|
+
type: "cf_agent_mcp_servers";
|
|
179
|
+
mcp: MCPServersState;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export type MCPServersState = {
|
|
183
|
+
servers: {
|
|
184
|
+
[id: string]: MCPServer;
|
|
185
|
+
};
|
|
186
|
+
tools: Tool[];
|
|
187
|
+
prompts: Prompt[];
|
|
188
|
+
resources: Resource[];
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export type MCPServer = {
|
|
192
|
+
name: string;
|
|
193
|
+
server_url: string;
|
|
194
|
+
auth_url: string | null;
|
|
195
|
+
// This state is specifically about the temporary process of getting a token (if needed).
|
|
196
|
+
// Scope outside of that can't be relied upon because when the DO sleeps, there's no way
|
|
197
|
+
// to communicate a change to a non-ready state.
|
|
198
|
+
state: "authenticating" | "connecting" | "ready" | "discovering" | "failed";
|
|
199
|
+
instructions: string | null;
|
|
200
|
+
capabilities: ServerCapabilities | null;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* MCP Server data stored in DO SQL for resuming MCP Server connections
|
|
205
|
+
*/
|
|
206
|
+
type MCPServerRow = {
|
|
207
|
+
id: string;
|
|
208
|
+
name: string;
|
|
209
|
+
server_url: string;
|
|
210
|
+
client_id: string | null;
|
|
211
|
+
auth_url: string | null;
|
|
212
|
+
callback_url: string;
|
|
213
|
+
server_options: string;
|
|
214
|
+
};
|
|
215
|
+
|
|
162
216
|
const STATE_ROW_ID = "cf_state_row_id";
|
|
163
217
|
const STATE_WAS_CHANGED = "cf_state_was_changed";
|
|
164
218
|
|
|
165
219
|
const DEFAULT_STATE = {} as unknown;
|
|
166
220
|
|
|
167
|
-
|
|
221
|
+
const agentContext = new AsyncLocalStorage<{
|
|
168
222
|
agent: Agent<unknown>;
|
|
169
223
|
connection: Connection | undefined;
|
|
170
224
|
request: Request | undefined;
|
|
225
|
+
email: AgentEmail | undefined;
|
|
171
226
|
}>();
|
|
172
227
|
|
|
228
|
+
export function getCurrentAgent<
|
|
229
|
+
T extends Agent<unknown, unknown> = Agent<unknown, unknown>
|
|
230
|
+
>(): {
|
|
231
|
+
agent: T | undefined;
|
|
232
|
+
connection: Connection | undefined;
|
|
233
|
+
request: Request<unknown, CfProperties<unknown>> | undefined;
|
|
234
|
+
} {
|
|
235
|
+
const store = agentContext.getStore() as
|
|
236
|
+
| {
|
|
237
|
+
agent: T;
|
|
238
|
+
connection: Connection | undefined;
|
|
239
|
+
request: Request<unknown, CfProperties<unknown>> | undefined;
|
|
240
|
+
}
|
|
241
|
+
| undefined;
|
|
242
|
+
if (!store) {
|
|
243
|
+
return {
|
|
244
|
+
agent: undefined,
|
|
245
|
+
connection: undefined,
|
|
246
|
+
request: undefined
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return store;
|
|
250
|
+
}
|
|
251
|
+
|
|
173
252
|
/**
|
|
174
253
|
* Base class for creating Agent implementations
|
|
175
254
|
* @template Env Environment type containing bindings
|
|
176
255
|
* @template State State type to store within the Agent
|
|
177
256
|
*/
|
|
178
257
|
export class Agent<Env, State = unknown> extends Server<Env> {
|
|
179
|
-
|
|
258
|
+
private _state = DEFAULT_STATE as State;
|
|
180
259
|
|
|
181
|
-
|
|
260
|
+
private _ParentClass: typeof Agent<Env, State> =
|
|
182
261
|
Object.getPrototypeOf(this).constructor;
|
|
183
262
|
|
|
184
|
-
mcp: MCPClientManager = new MCPClientManager(this
|
|
263
|
+
mcp: MCPClientManager = new MCPClientManager(this._ParentClass.name, "0.0.1");
|
|
185
264
|
|
|
186
265
|
/**
|
|
187
266
|
* Initial state for the Agent
|
|
@@ -193,9 +272,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
193
272
|
* Current state of the Agent
|
|
194
273
|
*/
|
|
195
274
|
get state(): State {
|
|
196
|
-
if (this
|
|
275
|
+
if (this._state !== DEFAULT_STATE) {
|
|
197
276
|
// state was previously set, and populated internal state
|
|
198
|
-
return this
|
|
277
|
+
return this._state;
|
|
199
278
|
}
|
|
200
279
|
// looks like this is the first time the state is being accessed
|
|
201
280
|
// check if the state was set in a previous life
|
|
@@ -215,8 +294,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
215
294
|
) {
|
|
216
295
|
const state = result[0]?.state as string; // could be null?
|
|
217
296
|
|
|
218
|
-
this
|
|
219
|
-
return this
|
|
297
|
+
this._state = JSON.parse(state);
|
|
298
|
+
return this._state;
|
|
220
299
|
}
|
|
221
300
|
|
|
222
301
|
// ok, this is the first time the state is being accessed
|
|
@@ -237,9 +316,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
237
316
|
*/
|
|
238
317
|
static options = {
|
|
239
318
|
/** Whether the Agent should hibernate when inactive */
|
|
240
|
-
hibernate: true
|
|
319
|
+
hibernate: true // default to hibernate
|
|
241
320
|
};
|
|
242
321
|
|
|
322
|
+
/**
|
|
323
|
+
* The observability implementation to use for the Agent
|
|
324
|
+
*/
|
|
325
|
+
observability?: Observability = genericObservability;
|
|
326
|
+
|
|
243
327
|
/**
|
|
244
328
|
* Execute SQL queries against the Agent's database
|
|
245
329
|
* @template T Type of the returned rows
|
|
@@ -277,7 +361,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
277
361
|
`;
|
|
278
362
|
|
|
279
363
|
void this.ctx.blockConcurrencyWhile(async () => {
|
|
280
|
-
return this
|
|
364
|
+
return this._tryCatch(async () => {
|
|
281
365
|
// Create alarms table if it doesn't exist
|
|
282
366
|
this.sql`
|
|
283
367
|
CREATE TABLE IF NOT EXISTS cf_agents_schedules (
|
|
@@ -297,25 +381,65 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
297
381
|
});
|
|
298
382
|
});
|
|
299
383
|
|
|
384
|
+
this.sql`
|
|
385
|
+
CREATE TABLE IF NOT EXISTS cf_agents_mcp_servers (
|
|
386
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
387
|
+
name TEXT NOT NULL,
|
|
388
|
+
server_url TEXT NOT NULL,
|
|
389
|
+
callback_url TEXT NOT NULL,
|
|
390
|
+
client_id TEXT,
|
|
391
|
+
auth_url TEXT,
|
|
392
|
+
server_options TEXT
|
|
393
|
+
)
|
|
394
|
+
`;
|
|
395
|
+
|
|
396
|
+
const _onRequest = this.onRequest.bind(this);
|
|
397
|
+
this.onRequest = (request: Request) => {
|
|
398
|
+
return agentContext.run(
|
|
399
|
+
{ agent: this, connection: undefined, request, email: undefined },
|
|
400
|
+
async () => {
|
|
401
|
+
if (this.mcp.isCallbackRequest(request)) {
|
|
402
|
+
await this.mcp.handleCallbackRequest(request);
|
|
403
|
+
|
|
404
|
+
// after the MCP connection handshake, we can send updated mcp state
|
|
405
|
+
this.broadcast(
|
|
406
|
+
JSON.stringify({
|
|
407
|
+
mcp: this.getMcpServers(),
|
|
408
|
+
type: "cf_agent_mcp_servers"
|
|
409
|
+
})
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
// We probably should let the user configure this response/redirect, but this is fine for now.
|
|
413
|
+
return new Response("<script>window.close();</script>", {
|
|
414
|
+
headers: { "content-type": "text/html" },
|
|
415
|
+
status: 200
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return this._tryCatch(() => _onRequest(request));
|
|
420
|
+
}
|
|
421
|
+
);
|
|
422
|
+
};
|
|
423
|
+
|
|
300
424
|
const _onMessage = this.onMessage.bind(this);
|
|
301
425
|
this.onMessage = async (connection: Connection, message: WSMessage) => {
|
|
302
|
-
return
|
|
303
|
-
{ agent: this, connection, request: undefined },
|
|
426
|
+
return agentContext.run(
|
|
427
|
+
{ agent: this, connection, request: undefined, email: undefined },
|
|
304
428
|
async () => {
|
|
305
429
|
if (typeof message !== "string") {
|
|
306
|
-
return this
|
|
430
|
+
return this._tryCatch(() => _onMessage(connection, message));
|
|
307
431
|
}
|
|
308
432
|
|
|
309
433
|
let parsed: unknown;
|
|
310
434
|
try {
|
|
311
435
|
parsed = JSON.parse(message);
|
|
312
|
-
} catch (
|
|
436
|
+
} catch (_e) {
|
|
313
437
|
// silently fail and let the onMessage handler handle it
|
|
314
|
-
return this
|
|
438
|
+
return this._tryCatch(() => _onMessage(connection, message));
|
|
315
439
|
}
|
|
316
440
|
|
|
317
441
|
if (isStateUpdateMessage(parsed)) {
|
|
318
|
-
this
|
|
442
|
+
this._setStateInternal(parsed.state as State, connection);
|
|
319
443
|
return;
|
|
320
444
|
}
|
|
321
445
|
|
|
@@ -329,11 +453,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
329
453
|
throw new Error(`Method ${method} does not exist`);
|
|
330
454
|
}
|
|
331
455
|
|
|
332
|
-
if (!this
|
|
456
|
+
if (!this._isCallable(method)) {
|
|
333
457
|
throw new Error(`Method ${method} is not callable`);
|
|
334
458
|
}
|
|
335
459
|
|
|
336
|
-
// biome-ignore lint/complexity/noBannedTypes: <explanation>
|
|
337
460
|
const metadata = callableMetadata.get(methodFn as Function);
|
|
338
461
|
|
|
339
462
|
// For streaming methods, pass a StreamingResponse object
|
|
@@ -345,22 +468,39 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
345
468
|
|
|
346
469
|
// For regular methods, execute and send response
|
|
347
470
|
const result = await methodFn.apply(this, args);
|
|
471
|
+
|
|
472
|
+
this.observability?.emit(
|
|
473
|
+
{
|
|
474
|
+
displayMessage: `RPC call to ${method}`,
|
|
475
|
+
id: nanoid(),
|
|
476
|
+
payload: {
|
|
477
|
+
args,
|
|
478
|
+
method,
|
|
479
|
+
streaming: metadata?.streaming,
|
|
480
|
+
success: true
|
|
481
|
+
},
|
|
482
|
+
timestamp: Date.now(),
|
|
483
|
+
type: "rpc"
|
|
484
|
+
},
|
|
485
|
+
this.ctx
|
|
486
|
+
);
|
|
487
|
+
|
|
348
488
|
const response: RPCResponse = {
|
|
349
|
-
|
|
489
|
+
done: true,
|
|
350
490
|
id,
|
|
351
|
-
success: true,
|
|
352
491
|
result,
|
|
353
|
-
|
|
492
|
+
success: true,
|
|
493
|
+
type: "rpc"
|
|
354
494
|
};
|
|
355
495
|
connection.send(JSON.stringify(response));
|
|
356
496
|
} catch (e) {
|
|
357
497
|
// Send error response
|
|
358
498
|
const response: RPCResponse = {
|
|
359
|
-
type: "rpc",
|
|
360
|
-
id: parsed.id,
|
|
361
|
-
success: false,
|
|
362
499
|
error:
|
|
363
500
|
e instanceof Error ? e.message : "Unknown error occurred",
|
|
501
|
+
id: parsed.id,
|
|
502
|
+
success: false,
|
|
503
|
+
type: "rpc"
|
|
364
504
|
};
|
|
365
505
|
connection.send(JSON.stringify(response));
|
|
366
506
|
console.error("RPC error:", e);
|
|
@@ -368,7 +508,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
368
508
|
return;
|
|
369
509
|
}
|
|
370
510
|
|
|
371
|
-
return this
|
|
511
|
+
return this._tryCatch(() => _onMessage(connection, message));
|
|
372
512
|
}
|
|
373
513
|
);
|
|
374
514
|
};
|
|
@@ -377,27 +517,94 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
377
517
|
this.onConnect = (connection: Connection, ctx: ConnectionContext) => {
|
|
378
518
|
// TODO: This is a hack to ensure the state is sent after the connection is established
|
|
379
519
|
// must fix this
|
|
380
|
-
return
|
|
381
|
-
{ agent: this, connection, request: ctx.request },
|
|
520
|
+
return agentContext.run(
|
|
521
|
+
{ agent: this, connection, request: ctx.request, email: undefined },
|
|
382
522
|
async () => {
|
|
383
523
|
setTimeout(() => {
|
|
384
524
|
if (this.state) {
|
|
385
525
|
connection.send(
|
|
386
526
|
JSON.stringify({
|
|
387
|
-
type: "cf_agent_state",
|
|
388
527
|
state: this.state,
|
|
528
|
+
type: "cf_agent_state"
|
|
389
529
|
})
|
|
390
530
|
);
|
|
391
531
|
}
|
|
392
|
-
|
|
532
|
+
|
|
533
|
+
connection.send(
|
|
534
|
+
JSON.stringify({
|
|
535
|
+
mcp: this.getMcpServers(),
|
|
536
|
+
type: "cf_agent_mcp_servers"
|
|
537
|
+
})
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
this.observability?.emit(
|
|
541
|
+
{
|
|
542
|
+
displayMessage: "Connection established",
|
|
543
|
+
id: nanoid(),
|
|
544
|
+
payload: {
|
|
545
|
+
connectionId: connection.id
|
|
546
|
+
},
|
|
547
|
+
timestamp: Date.now(),
|
|
548
|
+
type: "connect"
|
|
549
|
+
},
|
|
550
|
+
this.ctx
|
|
551
|
+
);
|
|
552
|
+
return this._tryCatch(() => _onConnect(connection, ctx));
|
|
393
553
|
}, 20);
|
|
394
554
|
}
|
|
395
555
|
);
|
|
396
556
|
};
|
|
557
|
+
|
|
558
|
+
const _onStart = this.onStart.bind(this);
|
|
559
|
+
this.onStart = async () => {
|
|
560
|
+
return agentContext.run(
|
|
561
|
+
{
|
|
562
|
+
agent: this,
|
|
563
|
+
connection: undefined,
|
|
564
|
+
request: undefined,
|
|
565
|
+
email: undefined
|
|
566
|
+
},
|
|
567
|
+
async () => {
|
|
568
|
+
const servers = this.sql<MCPServerRow>`
|
|
569
|
+
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
570
|
+
`;
|
|
571
|
+
|
|
572
|
+
// from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
|
|
573
|
+
Promise.allSettled(
|
|
574
|
+
servers.map((server) => {
|
|
575
|
+
return this._connectToMcpServerInternal(
|
|
576
|
+
server.name,
|
|
577
|
+
server.server_url,
|
|
578
|
+
server.callback_url,
|
|
579
|
+
server.server_options
|
|
580
|
+
? JSON.parse(server.server_options)
|
|
581
|
+
: undefined,
|
|
582
|
+
{
|
|
583
|
+
id: server.id,
|
|
584
|
+
oauthClientId: server.client_id ?? undefined
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
})
|
|
588
|
+
).then((_results) => {
|
|
589
|
+
this.broadcast(
|
|
590
|
+
JSON.stringify({
|
|
591
|
+
mcp: this.getMcpServers(),
|
|
592
|
+
type: "cf_agent_mcp_servers"
|
|
593
|
+
})
|
|
594
|
+
);
|
|
595
|
+
});
|
|
596
|
+
await this._tryCatch(() => _onStart());
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
};
|
|
397
600
|
}
|
|
398
601
|
|
|
399
|
-
|
|
400
|
-
|
|
602
|
+
private _setStateInternal(
|
|
603
|
+
state: State,
|
|
604
|
+
source: Connection | "server" = "server"
|
|
605
|
+
) {
|
|
606
|
+
const previousState = this._state;
|
|
607
|
+
this._state = state;
|
|
401
608
|
this.sql`
|
|
402
609
|
INSERT OR REPLACE INTO cf_agents_state (id, state)
|
|
403
610
|
VALUES (${STATE_ROW_ID}, ${JSON.stringify(state)})
|
|
@@ -408,16 +615,29 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
408
615
|
`;
|
|
409
616
|
this.broadcast(
|
|
410
617
|
JSON.stringify({
|
|
411
|
-
type: "cf_agent_state",
|
|
412
618
|
state: state,
|
|
619
|
+
type: "cf_agent_state"
|
|
413
620
|
}),
|
|
414
621
|
source !== "server" ? [source.id] : []
|
|
415
622
|
);
|
|
416
|
-
return this
|
|
417
|
-
const { connection, request } =
|
|
418
|
-
return
|
|
419
|
-
{ agent: this, connection, request },
|
|
623
|
+
return this._tryCatch(() => {
|
|
624
|
+
const { connection, request, email } = agentContext.getStore() || {};
|
|
625
|
+
return agentContext.run(
|
|
626
|
+
{ agent: this, connection, request, email },
|
|
420
627
|
async () => {
|
|
628
|
+
this.observability?.emit(
|
|
629
|
+
{
|
|
630
|
+
displayMessage: "State updated",
|
|
631
|
+
id: nanoid(),
|
|
632
|
+
payload: {
|
|
633
|
+
previousState,
|
|
634
|
+
state
|
|
635
|
+
},
|
|
636
|
+
timestamp: Date.now(),
|
|
637
|
+
type: "state:update"
|
|
638
|
+
},
|
|
639
|
+
this.ctx
|
|
640
|
+
);
|
|
421
641
|
return this.onStateUpdate(state, source);
|
|
422
642
|
}
|
|
423
643
|
);
|
|
@@ -429,7 +649,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
429
649
|
* @param state New state to set
|
|
430
650
|
*/
|
|
431
651
|
setState(state: State) {
|
|
432
|
-
this
|
|
652
|
+
this._setStateInternal(state, "server");
|
|
433
653
|
}
|
|
434
654
|
|
|
435
655
|
/**
|
|
@@ -437,24 +657,90 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
437
657
|
* @param state Updated state
|
|
438
658
|
* @param source Source of the state update ("server" or a client connection)
|
|
439
659
|
*/
|
|
660
|
+
// biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
|
|
440
661
|
onStateUpdate(state: State | undefined, source: Connection | "server") {
|
|
441
662
|
// override this to handle state updates
|
|
442
663
|
}
|
|
443
664
|
|
|
444
665
|
/**
|
|
445
|
-
* Called when the Agent receives an email
|
|
666
|
+
* Called when the Agent receives an email via routeAgentEmail()
|
|
667
|
+
* Override this method to handle incoming emails
|
|
446
668
|
* @param email Email message to process
|
|
447
669
|
*/
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
670
|
+
async _onEmail(email: AgentEmail) {
|
|
671
|
+
// nb: we use this roundabout way of getting to onEmail
|
|
672
|
+
// because of https://github.com/cloudflare/workerd/issues/4499
|
|
673
|
+
return agentContext.run(
|
|
674
|
+
{ agent: this, connection: undefined, request: undefined, email: email },
|
|
451
675
|
async () => {
|
|
452
|
-
|
|
676
|
+
if ("onEmail" in this && typeof this.onEmail === "function") {
|
|
677
|
+
return this._tryCatch(() =>
|
|
678
|
+
(this.onEmail as (email: AgentEmail) => Promise<void>)(email)
|
|
679
|
+
);
|
|
680
|
+
} else {
|
|
681
|
+
console.log("Received email from:", email.from, "to:", email.to);
|
|
682
|
+
console.log("Subject:", email.headers.get("subject"));
|
|
683
|
+
console.log(
|
|
684
|
+
"Implement onEmail(email: AgentEmail): Promise<void> in your agent to process emails"
|
|
685
|
+
);
|
|
686
|
+
}
|
|
453
687
|
}
|
|
454
688
|
);
|
|
455
689
|
}
|
|
456
690
|
|
|
457
|
-
|
|
691
|
+
/**
|
|
692
|
+
* Reply to an email
|
|
693
|
+
* @param email The email to reply to
|
|
694
|
+
* @param options Options for the reply
|
|
695
|
+
* @returns void
|
|
696
|
+
*/
|
|
697
|
+
async replyToEmail(
|
|
698
|
+
email: AgentEmail,
|
|
699
|
+
options: {
|
|
700
|
+
fromName: string;
|
|
701
|
+
subject?: string | undefined;
|
|
702
|
+
body: string;
|
|
703
|
+
contentType?: string;
|
|
704
|
+
headers?: Record<string, string>;
|
|
705
|
+
}
|
|
706
|
+
): Promise<void> {
|
|
707
|
+
return this._tryCatch(async () => {
|
|
708
|
+
const agentName = camelCaseToKebabCase(this._ParentClass.name);
|
|
709
|
+
const agentId = this.name;
|
|
710
|
+
|
|
711
|
+
const { createMimeMessage } = await import("mimetext");
|
|
712
|
+
const msg = createMimeMessage();
|
|
713
|
+
msg.setSender({ addr: email.to, name: options.fromName });
|
|
714
|
+
msg.setRecipient(email.from);
|
|
715
|
+
msg.setSubject(
|
|
716
|
+
options.subject || `Re: ${email.headers.get("subject")}` || "No subject"
|
|
717
|
+
);
|
|
718
|
+
msg.addMessage({
|
|
719
|
+
contentType: options.contentType || "text/plain",
|
|
720
|
+
data: options.body
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
const domain = email.from.split("@")[1];
|
|
724
|
+
const messageId = `<${agentId}@${domain}>`;
|
|
725
|
+
msg.setHeader("In-Reply-To", email.headers.get("Message-ID")!);
|
|
726
|
+
msg.setHeader("Message-ID", messageId);
|
|
727
|
+
msg.setHeader("X-Agent-Name", agentName);
|
|
728
|
+
msg.setHeader("X-Agent-ID", agentId);
|
|
729
|
+
|
|
730
|
+
if (options.headers) {
|
|
731
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
732
|
+
msg.setHeader(key, value);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
await email.reply({
|
|
736
|
+
from: email.to,
|
|
737
|
+
raw: msg.asRaw(),
|
|
738
|
+
to: email.from
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
private async _tryCatch<T>(fn: () => T | Promise<T>) {
|
|
458
744
|
try {
|
|
459
745
|
return await fn();
|
|
460
746
|
} catch (e) {
|
|
@@ -511,6 +797,18 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
511
797
|
): Promise<Schedule<T>> {
|
|
512
798
|
const id = nanoid(9);
|
|
513
799
|
|
|
800
|
+
const emitScheduleCreate = (schedule: Schedule<T>) =>
|
|
801
|
+
this.observability?.emit(
|
|
802
|
+
{
|
|
803
|
+
displayMessage: `Schedule ${schedule.id} created`,
|
|
804
|
+
id: nanoid(),
|
|
805
|
+
payload: schedule,
|
|
806
|
+
timestamp: Date.now(),
|
|
807
|
+
type: "schedule:create"
|
|
808
|
+
},
|
|
809
|
+
this.ctx
|
|
810
|
+
);
|
|
811
|
+
|
|
514
812
|
if (typeof callback !== "string") {
|
|
515
813
|
throw new Error("Callback must be a string");
|
|
516
814
|
}
|
|
@@ -528,15 +826,19 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
528
826
|
)}, 'scheduled', ${timestamp})
|
|
529
827
|
`;
|
|
530
828
|
|
|
531
|
-
await this
|
|
829
|
+
await this._scheduleNextAlarm();
|
|
532
830
|
|
|
533
|
-
|
|
534
|
-
id,
|
|
831
|
+
const schedule: Schedule<T> = {
|
|
535
832
|
callback: callback,
|
|
833
|
+
id,
|
|
536
834
|
payload: payload as T,
|
|
537
835
|
time: timestamp,
|
|
538
|
-
type: "scheduled"
|
|
836
|
+
type: "scheduled"
|
|
539
837
|
};
|
|
838
|
+
|
|
839
|
+
emitScheduleCreate(schedule);
|
|
840
|
+
|
|
841
|
+
return schedule;
|
|
540
842
|
}
|
|
541
843
|
if (typeof when === "number") {
|
|
542
844
|
const time = new Date(Date.now() + when * 1000);
|
|
@@ -549,16 +851,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
549
851
|
)}, 'delayed', ${when}, ${timestamp})
|
|
550
852
|
`;
|
|
551
853
|
|
|
552
|
-
await this
|
|
854
|
+
await this._scheduleNextAlarm();
|
|
553
855
|
|
|
554
|
-
|
|
555
|
-
id,
|
|
856
|
+
const schedule: Schedule<T> = {
|
|
556
857
|
callback: callback,
|
|
557
|
-
payload: payload as T,
|
|
558
858
|
delayInSeconds: when,
|
|
859
|
+
id,
|
|
860
|
+
payload: payload as T,
|
|
559
861
|
time: timestamp,
|
|
560
|
-
type: "delayed"
|
|
862
|
+
type: "delayed"
|
|
561
863
|
};
|
|
864
|
+
|
|
865
|
+
emitScheduleCreate(schedule);
|
|
866
|
+
|
|
867
|
+
return schedule;
|
|
562
868
|
}
|
|
563
869
|
if (typeof when === "string") {
|
|
564
870
|
const nextExecutionTime = getNextCronTime(when);
|
|
@@ -571,16 +877,20 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
571
877
|
)}, 'cron', ${when}, ${timestamp})
|
|
572
878
|
`;
|
|
573
879
|
|
|
574
|
-
await this
|
|
880
|
+
await this._scheduleNextAlarm();
|
|
575
881
|
|
|
576
|
-
|
|
577
|
-
id,
|
|
882
|
+
const schedule: Schedule<T> = {
|
|
578
883
|
callback: callback,
|
|
579
|
-
payload: payload as T,
|
|
580
884
|
cron: when,
|
|
885
|
+
id,
|
|
886
|
+
payload: payload as T,
|
|
581
887
|
time: timestamp,
|
|
582
|
-
type: "cron"
|
|
888
|
+
type: "cron"
|
|
583
889
|
};
|
|
890
|
+
|
|
891
|
+
emitScheduleCreate(schedule);
|
|
892
|
+
|
|
893
|
+
return schedule;
|
|
584
894
|
}
|
|
585
895
|
throw new Error("Invalid schedule type");
|
|
586
896
|
}
|
|
@@ -644,7 +954,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
644
954
|
.toArray()
|
|
645
955
|
.map((row) => ({
|
|
646
956
|
...row,
|
|
647
|
-
payload: JSON.parse(row.payload as string) as T
|
|
957
|
+
payload: JSON.parse(row.payload as string) as T
|
|
648
958
|
})) as Schedule<T>[];
|
|
649
959
|
|
|
650
960
|
return result;
|
|
@@ -656,13 +966,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
656
966
|
* @returns true if the task was cancelled, false otherwise
|
|
657
967
|
*/
|
|
658
968
|
async cancelSchedule(id: string): Promise<boolean> {
|
|
969
|
+
const schedule = await this.getSchedule(id);
|
|
970
|
+
if (schedule) {
|
|
971
|
+
this.observability?.emit(
|
|
972
|
+
{
|
|
973
|
+
displayMessage: `Schedule ${id} cancelled`,
|
|
974
|
+
id: nanoid(),
|
|
975
|
+
payload: schedule,
|
|
976
|
+
timestamp: Date.now(),
|
|
977
|
+
type: "schedule:cancel"
|
|
978
|
+
},
|
|
979
|
+
this.ctx
|
|
980
|
+
);
|
|
981
|
+
}
|
|
659
982
|
this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
|
|
660
983
|
|
|
661
|
-
await this
|
|
984
|
+
await this._scheduleNextAlarm();
|
|
662
985
|
return true;
|
|
663
986
|
}
|
|
664
987
|
|
|
665
|
-
async
|
|
988
|
+
private async _scheduleNextAlarm() {
|
|
666
989
|
// Find the next schedule that needs to be executed
|
|
667
990
|
const result = this.sql`
|
|
668
991
|
SELECT time FROM cf_agents_schedules
|
|
@@ -679,10 +1002,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
679
1002
|
}
|
|
680
1003
|
|
|
681
1004
|
/**
|
|
682
|
-
* Method called when an alarm fires
|
|
683
|
-
* Executes any scheduled tasks that are due
|
|
1005
|
+
* Method called when an alarm fires.
|
|
1006
|
+
* Executes any scheduled tasks that are due.
|
|
1007
|
+
*
|
|
1008
|
+
* @remarks
|
|
1009
|
+
* To schedule a task, please use the `this.schedule` method instead.
|
|
1010
|
+
* See {@link https://developers.cloudflare.com/agents/api-reference/schedule-tasks/}
|
|
684
1011
|
*/
|
|
685
|
-
async
|
|
1012
|
+
public readonly alarm = async () => {
|
|
686
1013
|
const now = Math.floor(Date.now() / 1000);
|
|
687
1014
|
|
|
688
1015
|
// Get all schedules that should be executed now
|
|
@@ -696,10 +1023,26 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
696
1023
|
console.error(`callback ${row.callback} not found`);
|
|
697
1024
|
continue;
|
|
698
1025
|
}
|
|
699
|
-
await
|
|
700
|
-
{
|
|
1026
|
+
await agentContext.run(
|
|
1027
|
+
{
|
|
1028
|
+
agent: this,
|
|
1029
|
+
connection: undefined,
|
|
1030
|
+
request: undefined,
|
|
1031
|
+
email: undefined
|
|
1032
|
+
},
|
|
701
1033
|
async () => {
|
|
702
1034
|
try {
|
|
1035
|
+
this.observability?.emit(
|
|
1036
|
+
{
|
|
1037
|
+
displayMessage: `Schedule ${row.id} executed`,
|
|
1038
|
+
id: nanoid(),
|
|
1039
|
+
payload: row,
|
|
1040
|
+
timestamp: Date.now(),
|
|
1041
|
+
type: "schedule:execute"
|
|
1042
|
+
},
|
|
1043
|
+
this.ctx
|
|
1044
|
+
);
|
|
1045
|
+
|
|
703
1046
|
await (
|
|
704
1047
|
callback as (
|
|
705
1048
|
payload: unknown,
|
|
@@ -728,8 +1071,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
728
1071
|
}
|
|
729
1072
|
|
|
730
1073
|
// Schedule the next alarm
|
|
731
|
-
await this
|
|
732
|
-
}
|
|
1074
|
+
await this._scheduleNextAlarm();
|
|
1075
|
+
};
|
|
733
1076
|
|
|
734
1077
|
/**
|
|
735
1078
|
* Destroy the Agent, removing all state and scheduled tasks
|
|
@@ -738,20 +1081,200 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
738
1081
|
// drop all tables
|
|
739
1082
|
this.sql`DROP TABLE IF EXISTS cf_agents_state`;
|
|
740
1083
|
this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
|
|
1084
|
+
this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
|
|
741
1085
|
|
|
742
1086
|
// delete all alarms
|
|
743
1087
|
await this.ctx.storage.deleteAlarm();
|
|
744
1088
|
await this.ctx.storage.deleteAll();
|
|
1089
|
+
this.ctx.abort("destroyed"); // enforce that the agent is evicted
|
|
1090
|
+
|
|
1091
|
+
this.observability?.emit(
|
|
1092
|
+
{
|
|
1093
|
+
displayMessage: "Agent destroyed",
|
|
1094
|
+
id: nanoid(),
|
|
1095
|
+
payload: {},
|
|
1096
|
+
timestamp: Date.now(),
|
|
1097
|
+
type: "destroy"
|
|
1098
|
+
},
|
|
1099
|
+
this.ctx
|
|
1100
|
+
);
|
|
745
1101
|
}
|
|
746
1102
|
|
|
747
1103
|
/**
|
|
748
1104
|
* Get all methods marked as callable on this Agent
|
|
749
1105
|
* @returns A map of method names to their metadata
|
|
750
1106
|
*/
|
|
751
|
-
|
|
752
|
-
// biome-ignore lint/complexity/noBannedTypes: <explanation>
|
|
1107
|
+
private _isCallable(method: string): boolean {
|
|
753
1108
|
return callableMetadata.has(this[method as keyof this] as Function);
|
|
754
1109
|
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Connect to a new MCP Server
|
|
1113
|
+
*
|
|
1114
|
+
* @param url MCP Server SSE URL
|
|
1115
|
+
* @param callbackHost Base host for the agent, used for the redirect URI.
|
|
1116
|
+
* @param agentsPrefix agents routing prefix if not using `agents`
|
|
1117
|
+
* @param options MCP client and transport (header) options
|
|
1118
|
+
* @returns authUrl
|
|
1119
|
+
*/
|
|
1120
|
+
async addMcpServer(
|
|
1121
|
+
serverName: string,
|
|
1122
|
+
url: string,
|
|
1123
|
+
callbackHost: string,
|
|
1124
|
+
agentsPrefix = "agents",
|
|
1125
|
+
options?: {
|
|
1126
|
+
client?: ConstructorParameters<typeof Client>[1];
|
|
1127
|
+
transport?: {
|
|
1128
|
+
headers: HeadersInit;
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
): Promise<{ id: string; authUrl: string | undefined }> {
|
|
1132
|
+
const callbackUrl = `${callbackHost}/${agentsPrefix}/${camelCaseToKebabCase(this._ParentClass.name)}/${this.name}/callback`;
|
|
1133
|
+
|
|
1134
|
+
const result = await this._connectToMcpServerInternal(
|
|
1135
|
+
serverName,
|
|
1136
|
+
url,
|
|
1137
|
+
callbackUrl,
|
|
1138
|
+
options
|
|
1139
|
+
);
|
|
1140
|
+
this.sql`
|
|
1141
|
+
INSERT
|
|
1142
|
+
OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
|
|
1143
|
+
VALUES (
|
|
1144
|
+
${result.id},
|
|
1145
|
+
${serverName},
|
|
1146
|
+
${url},
|
|
1147
|
+
${result.clientId ?? null},
|
|
1148
|
+
${result.authUrl ?? null},
|
|
1149
|
+
${callbackUrl},
|
|
1150
|
+
${options ? JSON.stringify(options) : null}
|
|
1151
|
+
);
|
|
1152
|
+
`;
|
|
1153
|
+
|
|
1154
|
+
this.broadcast(
|
|
1155
|
+
JSON.stringify({
|
|
1156
|
+
mcp: this.getMcpServers(),
|
|
1157
|
+
type: "cf_agent_mcp_servers"
|
|
1158
|
+
})
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
return result;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
async _connectToMcpServerInternal(
|
|
1165
|
+
_serverName: string,
|
|
1166
|
+
url: string,
|
|
1167
|
+
callbackUrl: string,
|
|
1168
|
+
// it's important that any options here are serializable because we put them into our sqlite DB for reconnection purposes
|
|
1169
|
+
options?: {
|
|
1170
|
+
client?: ConstructorParameters<typeof Client>[1];
|
|
1171
|
+
/**
|
|
1172
|
+
* We don't expose the normal set of transport options because:
|
|
1173
|
+
* 1) we can't serialize things like the auth provider or a fetch function into the DB for reconnection purposes
|
|
1174
|
+
* 2) We probably want these options to be agnostic to the transport type (SSE vs Streamable)
|
|
1175
|
+
*
|
|
1176
|
+
* 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).
|
|
1177
|
+
*/
|
|
1178
|
+
transport?: {
|
|
1179
|
+
headers?: HeadersInit;
|
|
1180
|
+
};
|
|
1181
|
+
},
|
|
1182
|
+
reconnect?: {
|
|
1183
|
+
id: string;
|
|
1184
|
+
oauthClientId?: string;
|
|
1185
|
+
}
|
|
1186
|
+
): Promise<{
|
|
1187
|
+
id: string;
|
|
1188
|
+
authUrl: string | undefined;
|
|
1189
|
+
clientId: string | undefined;
|
|
1190
|
+
}> {
|
|
1191
|
+
const authProvider = new DurableObjectOAuthClientProvider(
|
|
1192
|
+
this.ctx.storage,
|
|
1193
|
+
this.name,
|
|
1194
|
+
callbackUrl
|
|
1195
|
+
);
|
|
1196
|
+
|
|
1197
|
+
if (reconnect) {
|
|
1198
|
+
authProvider.serverId = reconnect.id;
|
|
1199
|
+
if (reconnect.oauthClientId) {
|
|
1200
|
+
authProvider.clientId = reconnect.oauthClientId;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// allows passing through transport headers if necessary
|
|
1205
|
+
// this handles some non-standard bearer auth setups (i.e. MCP server behind CF access instead of OAuth)
|
|
1206
|
+
let headerTransportOpts: SSEClientTransportOptions = {};
|
|
1207
|
+
if (options?.transport?.headers) {
|
|
1208
|
+
headerTransportOpts = {
|
|
1209
|
+
eventSourceInit: {
|
|
1210
|
+
fetch: (url, init) =>
|
|
1211
|
+
fetch(url, {
|
|
1212
|
+
...init,
|
|
1213
|
+
headers: options?.transport?.headers
|
|
1214
|
+
})
|
|
1215
|
+
},
|
|
1216
|
+
requestInit: {
|
|
1217
|
+
headers: options?.transport?.headers
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const { id, authUrl, clientId } = await this.mcp.connect(url, {
|
|
1223
|
+
client: options?.client,
|
|
1224
|
+
reconnect,
|
|
1225
|
+
transport: {
|
|
1226
|
+
...headerTransportOpts,
|
|
1227
|
+
authProvider
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
return {
|
|
1232
|
+
authUrl,
|
|
1233
|
+
clientId,
|
|
1234
|
+
id
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
async removeMcpServer(id: string) {
|
|
1239
|
+
this.mcp.closeConnection(id);
|
|
1240
|
+
this.sql`
|
|
1241
|
+
DELETE FROM cf_agents_mcp_servers WHERE id = ${id};
|
|
1242
|
+
`;
|
|
1243
|
+
this.broadcast(
|
|
1244
|
+
JSON.stringify({
|
|
1245
|
+
mcp: this.getMcpServers(),
|
|
1246
|
+
type: "cf_agent_mcp_servers"
|
|
1247
|
+
})
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
getMcpServers(): MCPServersState {
|
|
1252
|
+
const mcpState: MCPServersState = {
|
|
1253
|
+
prompts: this.mcp.listPrompts(),
|
|
1254
|
+
resources: this.mcp.listResources(),
|
|
1255
|
+
servers: {},
|
|
1256
|
+
tools: this.mcp.listTools()
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
const servers = this.sql<MCPServerRow>`
|
|
1260
|
+
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
1261
|
+
`;
|
|
1262
|
+
|
|
1263
|
+
for (const server of servers) {
|
|
1264
|
+
const serverConn = this.mcp.mcpConnections[server.id];
|
|
1265
|
+
mcpState.servers[server.id] = {
|
|
1266
|
+
auth_url: server.auth_url,
|
|
1267
|
+
capabilities: serverConn?.serverCapabilities ?? null,
|
|
1268
|
+
instructions: serverConn?.instructions ?? null,
|
|
1269
|
+
name: server.name,
|
|
1270
|
+
server_url: server.server_url,
|
|
1271
|
+
// mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
|
|
1272
|
+
state: serverConn?.connectionState ?? "authenticating"
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
return mcpState;
|
|
1277
|
+
}
|
|
755
1278
|
}
|
|
756
1279
|
|
|
757
1280
|
/**
|
|
@@ -791,17 +1314,17 @@ export async function routeAgentRequest<Env>(
|
|
|
791
1314
|
const corsHeaders =
|
|
792
1315
|
options?.cors === true
|
|
793
1316
|
? {
|
|
794
|
-
"Access-Control-Allow-Origin": "*",
|
|
795
|
-
"Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
|
|
796
1317
|
"Access-Control-Allow-Credentials": "true",
|
|
797
|
-
"Access-Control-
|
|
1318
|
+
"Access-Control-Allow-Methods": "GET, POST, HEAD, OPTIONS",
|
|
1319
|
+
"Access-Control-Allow-Origin": "*",
|
|
1320
|
+
"Access-Control-Max-Age": "86400"
|
|
798
1321
|
}
|
|
799
1322
|
: options?.cors;
|
|
800
1323
|
|
|
801
1324
|
if (request.method === "OPTIONS") {
|
|
802
1325
|
if (corsHeaders) {
|
|
803
1326
|
return new Response(null, {
|
|
804
|
-
headers: corsHeaders
|
|
1327
|
+
headers: corsHeaders
|
|
805
1328
|
});
|
|
806
1329
|
}
|
|
807
1330
|
console.warn(
|
|
@@ -814,7 +1337,7 @@ export async function routeAgentRequest<Env>(
|
|
|
814
1337
|
env as Record<string, unknown>,
|
|
815
1338
|
{
|
|
816
1339
|
prefix: "agents",
|
|
817
|
-
...(options as PartyServerOptions<Record<string, unknown>>)
|
|
1340
|
+
...(options as PartyServerOptions<Record<string, unknown>>)
|
|
818
1341
|
}
|
|
819
1342
|
);
|
|
820
1343
|
|
|
@@ -827,24 +1350,218 @@ export async function routeAgentRequest<Env>(
|
|
|
827
1350
|
response = new Response(response.body, {
|
|
828
1351
|
headers: {
|
|
829
1352
|
...response.headers,
|
|
830
|
-
...corsHeaders
|
|
831
|
-
}
|
|
1353
|
+
...corsHeaders
|
|
1354
|
+
}
|
|
832
1355
|
});
|
|
833
1356
|
}
|
|
834
1357
|
return response;
|
|
835
1358
|
}
|
|
836
1359
|
|
|
1360
|
+
export type EmailResolver<Env> = (
|
|
1361
|
+
email: ForwardableEmailMessage,
|
|
1362
|
+
env: Env
|
|
1363
|
+
) => Promise<{
|
|
1364
|
+
agentName: string;
|
|
1365
|
+
agentId: string;
|
|
1366
|
+
} | null>;
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* Create a resolver that uses the message-id header to determine the agent to route the email to
|
|
1370
|
+
* @returns A function that resolves the agent to route the email to
|
|
1371
|
+
*/
|
|
1372
|
+
export function createHeaderBasedEmailResolver<Env>(): EmailResolver<Env> {
|
|
1373
|
+
return async (email: ForwardableEmailMessage, _env: Env) => {
|
|
1374
|
+
const messageId = email.headers.get("message-id");
|
|
1375
|
+
if (messageId) {
|
|
1376
|
+
const messageIdMatch = messageId.match(/<([^@]+)@([^>]+)>/);
|
|
1377
|
+
if (messageIdMatch) {
|
|
1378
|
+
const [, agentId, domain] = messageIdMatch;
|
|
1379
|
+
const agentName = domain.split(".")[0];
|
|
1380
|
+
return { agentName, agentId };
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const references = email.headers.get("references");
|
|
1385
|
+
if (references) {
|
|
1386
|
+
const referencesMatch = references.match(
|
|
1387
|
+
/<([A-Za-z0-9+/]{43}=)@([^>]+)>/
|
|
1388
|
+
);
|
|
1389
|
+
if (referencesMatch) {
|
|
1390
|
+
const [, base64Id, domain] = referencesMatch;
|
|
1391
|
+
const agentId = Buffer.from(base64Id, "base64").toString("hex");
|
|
1392
|
+
const agentName = domain.split(".")[0];
|
|
1393
|
+
return { agentName, agentId };
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const agentName = email.headers.get("x-agent-name");
|
|
1398
|
+
const agentId = email.headers.get("x-agent-id");
|
|
1399
|
+
if (agentName && agentId) {
|
|
1400
|
+
return { agentName, agentId };
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
return null;
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* Create a resolver that uses the email address to determine the agent to route the email to
|
|
1409
|
+
* @param defaultAgentName The default agent name to use if the email address does not contain a sub-address
|
|
1410
|
+
* @returns A function that resolves the agent to route the email to
|
|
1411
|
+
*/
|
|
1412
|
+
export function createAddressBasedEmailResolver<Env>(
|
|
1413
|
+
defaultAgentName: string
|
|
1414
|
+
): EmailResolver<Env> {
|
|
1415
|
+
return async (email: ForwardableEmailMessage, _env: Env) => {
|
|
1416
|
+
const emailMatch = email.to.match(/^([^+@]+)(?:\+([^@]+))?@(.+)$/);
|
|
1417
|
+
if (!emailMatch) {
|
|
1418
|
+
return null;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
const [, localPart, subAddress] = emailMatch;
|
|
1422
|
+
|
|
1423
|
+
if (subAddress) {
|
|
1424
|
+
return {
|
|
1425
|
+
agentName: localPart,
|
|
1426
|
+
agentId: subAddress
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Option 2: Use defaultAgentName namespace, localPart as agentId
|
|
1431
|
+
// Common for catch-all email routing to a single EmailAgent namespace
|
|
1432
|
+
return {
|
|
1433
|
+
agentName: defaultAgentName,
|
|
1434
|
+
agentId: localPart
|
|
1435
|
+
};
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Create a resolver that uses the agentName and agentId to determine the agent to route the email to
|
|
1441
|
+
* @param agentName The name of the agent to route the email to
|
|
1442
|
+
* @param agentId The id of the agent to route the email to
|
|
1443
|
+
* @returns A function that resolves the agent to route the email to
|
|
1444
|
+
*/
|
|
1445
|
+
export function createCatchAllEmailResolver<Env>(
|
|
1446
|
+
agentName: string,
|
|
1447
|
+
agentId: string
|
|
1448
|
+
): EmailResolver<Env> {
|
|
1449
|
+
return async () => ({ agentName, agentId });
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
export type EmailRoutingOptions<Env> = AgentOptions<Env> & {
|
|
1453
|
+
resolver: EmailResolver<Env>;
|
|
1454
|
+
};
|
|
1455
|
+
|
|
837
1456
|
/**
|
|
838
1457
|
* Route an email to the appropriate Agent
|
|
839
|
-
* @param email
|
|
840
|
-
* @param env
|
|
841
|
-
* @param options
|
|
1458
|
+
* @param email The email to route
|
|
1459
|
+
* @param env The environment containing the Agent bindings
|
|
1460
|
+
* @param options The options for routing the email
|
|
1461
|
+
* @returns A promise that resolves when the email has been routed
|
|
842
1462
|
*/
|
|
843
1463
|
export async function routeAgentEmail<Env>(
|
|
844
1464
|
email: ForwardableEmailMessage,
|
|
845
1465
|
env: Env,
|
|
846
|
-
options
|
|
847
|
-
): Promise<void> {
|
|
1466
|
+
options: EmailRoutingOptions<Env>
|
|
1467
|
+
): Promise<void> {
|
|
1468
|
+
const routingInfo = await options.resolver(email, env);
|
|
1469
|
+
|
|
1470
|
+
if (!routingInfo) {
|
|
1471
|
+
console.warn("No routing information found for email, dropping message");
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const namespaceBinding = env[routingInfo.agentName as keyof Env];
|
|
1476
|
+
if (!namespaceBinding) {
|
|
1477
|
+
throw new Error(
|
|
1478
|
+
`Agent namespace '${routingInfo.agentName}' not found in environment`
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// Type guard to check if this is actually a DurableObjectNamespace (AgentNamespace)
|
|
1483
|
+
if (
|
|
1484
|
+
typeof namespaceBinding !== "object" ||
|
|
1485
|
+
!("idFromName" in namespaceBinding) ||
|
|
1486
|
+
typeof namespaceBinding.idFromName !== "function"
|
|
1487
|
+
) {
|
|
1488
|
+
throw new Error(
|
|
1489
|
+
`Environment binding '${routingInfo.agentName}' is not an AgentNamespace (found: ${typeof namespaceBinding})`
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// Safe cast after runtime validation
|
|
1494
|
+
const namespace = namespaceBinding as unknown as AgentNamespace<Agent<Env>>;
|
|
1495
|
+
|
|
1496
|
+
const agent = await getAgentByName(namespace, routingInfo.agentId);
|
|
1497
|
+
|
|
1498
|
+
// let's make a serialisable version of the email
|
|
1499
|
+
const serialisableEmail: AgentEmail = {
|
|
1500
|
+
getRaw: async () => {
|
|
1501
|
+
const reader = email.raw.getReader();
|
|
1502
|
+
const chunks: Uint8Array[] = [];
|
|
1503
|
+
|
|
1504
|
+
let done = false;
|
|
1505
|
+
while (!done) {
|
|
1506
|
+
const { value, done: readerDone } = await reader.read();
|
|
1507
|
+
done = readerDone;
|
|
1508
|
+
if (value) {
|
|
1509
|
+
chunks.push(value);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
1514
|
+
const combined = new Uint8Array(totalLength);
|
|
1515
|
+
let offset = 0;
|
|
1516
|
+
for (const chunk of chunks) {
|
|
1517
|
+
combined.set(chunk, offset);
|
|
1518
|
+
offset += chunk.length;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
return combined;
|
|
1522
|
+
},
|
|
1523
|
+
headers: email.headers,
|
|
1524
|
+
rawSize: email.rawSize,
|
|
1525
|
+
setReject: (reason: string) => {
|
|
1526
|
+
email.setReject(reason);
|
|
1527
|
+
},
|
|
1528
|
+
forward: (rcptTo: string, headers?: Headers) => {
|
|
1529
|
+
return email.forward(rcptTo, headers);
|
|
1530
|
+
},
|
|
1531
|
+
reply: (options: { from: string; to: string; raw: string }) => {
|
|
1532
|
+
return email.reply(
|
|
1533
|
+
new EmailMessage(options.from, options.to, options.raw)
|
|
1534
|
+
);
|
|
1535
|
+
},
|
|
1536
|
+
from: email.from,
|
|
1537
|
+
to: email.to
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1540
|
+
await agent._onEmail(serialisableEmail);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
export type AgentEmail = {
|
|
1544
|
+
from: string;
|
|
1545
|
+
to: string;
|
|
1546
|
+
getRaw: () => Promise<Uint8Array>;
|
|
1547
|
+
headers: Headers;
|
|
1548
|
+
rawSize: number;
|
|
1549
|
+
setReject: (reason: string) => void;
|
|
1550
|
+
forward: (rcptTo: string, headers?: Headers) => Promise<void>;
|
|
1551
|
+
reply: (options: { from: string; to: string; raw: string }) => Promise<void>;
|
|
1552
|
+
};
|
|
1553
|
+
|
|
1554
|
+
export type EmailSendOptions = {
|
|
1555
|
+
to: string;
|
|
1556
|
+
subject: string;
|
|
1557
|
+
body: string;
|
|
1558
|
+
contentType?: string;
|
|
1559
|
+
headers?: Record<string, string>;
|
|
1560
|
+
includeRoutingHeaders?: boolean;
|
|
1561
|
+
agentName?: string;
|
|
1562
|
+
agentId?: string;
|
|
1563
|
+
domain?: string;
|
|
1564
|
+
};
|
|
848
1565
|
|
|
849
1566
|
/**
|
|
850
1567
|
* Get or create an Agent by name
|
|
@@ -855,7 +1572,7 @@ export async function routeAgentEmail<Env>(
|
|
|
855
1572
|
* @param options Options for Agent creation
|
|
856
1573
|
* @returns Promise resolving to an Agent instance stub
|
|
857
1574
|
*/
|
|
858
|
-
export function getAgentByName<Env, T extends Agent<Env>>(
|
|
1575
|
+
export async function getAgentByName<Env, T extends Agent<Env>>(
|
|
859
1576
|
namespace: AgentNamespace<T>,
|
|
860
1577
|
name: string,
|
|
861
1578
|
options?: {
|
|
@@ -870,13 +1587,13 @@ export function getAgentByName<Env, T extends Agent<Env>>(
|
|
|
870
1587
|
* A wrapper for streaming responses in callable methods
|
|
871
1588
|
*/
|
|
872
1589
|
export class StreamingResponse {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1590
|
+
private _connection: Connection;
|
|
1591
|
+
private _id: string;
|
|
1592
|
+
private _closed = false;
|
|
876
1593
|
|
|
877
1594
|
constructor(connection: Connection, id: string) {
|
|
878
|
-
this
|
|
879
|
-
this
|
|
1595
|
+
this._connection = connection;
|
|
1596
|
+
this._id = id;
|
|
880
1597
|
}
|
|
881
1598
|
|
|
882
1599
|
/**
|
|
@@ -884,17 +1601,17 @@ export class StreamingResponse {
|
|
|
884
1601
|
* @param chunk The data to send
|
|
885
1602
|
*/
|
|
886
1603
|
send(chunk: unknown) {
|
|
887
|
-
if (this
|
|
1604
|
+
if (this._closed) {
|
|
888
1605
|
throw new Error("StreamingResponse is already closed");
|
|
889
1606
|
}
|
|
890
1607
|
const response: RPCResponse = {
|
|
891
|
-
type: "rpc",
|
|
892
|
-
id: this.#id,
|
|
893
|
-
success: true,
|
|
894
|
-
result: chunk,
|
|
895
1608
|
done: false,
|
|
1609
|
+
id: this._id,
|
|
1610
|
+
result: chunk,
|
|
1611
|
+
success: true,
|
|
1612
|
+
type: "rpc"
|
|
896
1613
|
};
|
|
897
|
-
this
|
|
1614
|
+
this._connection.send(JSON.stringify(response));
|
|
898
1615
|
}
|
|
899
1616
|
|
|
900
1617
|
/**
|
|
@@ -902,17 +1619,17 @@ export class StreamingResponse {
|
|
|
902
1619
|
* @param finalChunk Optional final chunk of data to send
|
|
903
1620
|
*/
|
|
904
1621
|
end(finalChunk?: unknown) {
|
|
905
|
-
if (this
|
|
1622
|
+
if (this._closed) {
|
|
906
1623
|
throw new Error("StreamingResponse is already closed");
|
|
907
1624
|
}
|
|
908
|
-
this
|
|
1625
|
+
this._closed = true;
|
|
909
1626
|
const response: RPCResponse = {
|
|
910
|
-
type: "rpc",
|
|
911
|
-
id: this.#id,
|
|
912
|
-
success: true,
|
|
913
|
-
result: finalChunk,
|
|
914
1627
|
done: true,
|
|
1628
|
+
id: this._id,
|
|
1629
|
+
result: finalChunk,
|
|
1630
|
+
success: true,
|
|
1631
|
+
type: "rpc"
|
|
915
1632
|
};
|
|
916
|
-
this
|
|
1633
|
+
this._connection.send(JSON.stringify(response));
|
|
917
1634
|
}
|
|
918
1635
|
}
|