agents 0.0.0-c3e8618 → 0.0.0-c4c9271
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/dist/ai-chat-agent.d.ts +73 -23
- package/dist/ai-chat-agent.js +230 -0
- package/dist/ai-chat-agent.js.map +1 -0
- package/dist/ai-react.d.ts +85 -44
- package/dist/ai-react.js +203 -0
- package/dist/ai-react.js.map +1 -0
- package/dist/ai-types.d.ts +65 -40
- package/dist/ai-types.js +1 -0
- package/dist/ai-types.js.map +1 -0
- package/dist/chunk-6RPGDIE2.js +786 -0
- package/dist/chunk-6RPGDIE2.js.map +1 -0
- package/dist/chunk-BZXOAZUX.js +106 -0
- package/dist/chunk-BZXOAZUX.js.map +1 -0
- package/dist/chunk-OYJXQRRH.js +465 -0
- package/dist/chunk-OYJXQRRH.js.map +1 -0
- package/dist/chunk-VCSB47AK.js +116 -0
- package/dist/chunk-VCSB47AK.js.map +1 -0
- package/dist/client.d.ts +71 -37
- package/dist/client.js +11 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +331 -179
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/client.d.ts +142 -34
- package/dist/mcp/client.js +9 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/do-oauth-client-provider.d.ts +41 -0
- package/dist/mcp/do-oauth-client-provider.js +7 -0
- package/dist/mcp/do-oauth-client-provider.js.map +1 -0
- package/dist/mcp/index.d.ts +50 -7
- package/dist/mcp/index.js +782 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/react.d.ts +104 -15
- package/dist/react.js +116 -0
- package/dist/react.js.map +1 -0
- package/dist/schedule.d.ts +30 -20
- package/dist/schedule.js +71 -0
- package/dist/schedule.js.map +1 -0
- package/dist/serializable.d.ts +32 -0
- package/dist/serializable.js +1 -0
- package/dist/serializable.js.map +1 -0
- package/package.json +28 -5
- package/src/index.ts +396 -60
package/src/index.ts
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Server,
|
|
3
|
-
routePartykitRequest,
|
|
4
|
-
type PartyServerOptions,
|
|
5
3
|
getServerByName,
|
|
4
|
+
routePartykitRequest,
|
|
6
5
|
type Connection,
|
|
7
6
|
type ConnectionContext,
|
|
7
|
+
type PartyServerOptions,
|
|
8
8
|
type WSMessage,
|
|
9
9
|
} from "partyserver";
|
|
10
10
|
|
|
11
11
|
import { parseCronExpression } from "cron-schedule";
|
|
12
12
|
import { nanoid } from "nanoid";
|
|
13
13
|
|
|
14
|
+
import type {
|
|
15
|
+
Prompt,
|
|
16
|
+
Resource,
|
|
17
|
+
ServerCapabilities,
|
|
18
|
+
Tool,
|
|
19
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
20
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
21
|
+
import { MCPClientManager } from "./mcp/client";
|
|
22
|
+
import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider";
|
|
15
23
|
|
|
16
|
-
|
|
24
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
25
|
+
import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
17
26
|
|
|
18
|
-
import {
|
|
27
|
+
import { camelCaseToKebabCase } from "./client";
|
|
28
|
+
import type { MCPClientConnection } from "./mcp/client-connection";
|
|
29
|
+
|
|
30
|
+
export type { Connection, ConnectionContext, WSMessage } from "partyserver";
|
|
19
31
|
|
|
20
32
|
/**
|
|
21
33
|
* RPC request message from client
|
|
@@ -99,7 +111,6 @@ export type CallableMetadata = {
|
|
|
99
111
|
streaming?: boolean;
|
|
100
112
|
};
|
|
101
113
|
|
|
102
|
-
// biome-ignore lint/complexity/noBannedTypes: <explanation>
|
|
103
114
|
const callableMetadata = new Map<Function, CallableMetadata>();
|
|
104
115
|
|
|
105
116
|
/**
|
|
@@ -119,11 +130,6 @@ export function unstable_callable(metadata: CallableMetadata = {}) {
|
|
|
119
130
|
};
|
|
120
131
|
}
|
|
121
132
|
|
|
122
|
-
/**
|
|
123
|
-
* A class for creating workflow entry points that can be used with Cloudflare Workers
|
|
124
|
-
*/
|
|
125
|
-
export class WorkflowEntrypoint extends CFWorkflowEntrypoint {}
|
|
126
|
-
|
|
127
133
|
/**
|
|
128
134
|
* Represents a scheduled task within an Agent
|
|
129
135
|
* @template T Type of the payload data
|
|
@@ -165,24 +171,95 @@ function getNextCronTime(cron: string) {
|
|
|
165
171
|
return interval.getNextDate();
|
|
166
172
|
}
|
|
167
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
|
+
|
|
168
216
|
const STATE_ROW_ID = "cf_state_row_id";
|
|
169
217
|
const STATE_WAS_CHANGED = "cf_state_was_changed";
|
|
170
218
|
|
|
171
219
|
const DEFAULT_STATE = {} as unknown;
|
|
172
220
|
|
|
173
|
-
|
|
221
|
+
const agentContext = new AsyncLocalStorage<{
|
|
174
222
|
agent: Agent<unknown>;
|
|
175
223
|
connection: Connection | undefined;
|
|
176
224
|
request: Request | undefined;
|
|
177
225
|
}>();
|
|
178
226
|
|
|
227
|
+
export function getCurrentAgent<
|
|
228
|
+
T extends Agent<unknown, unknown> = Agent<unknown, unknown>,
|
|
229
|
+
>(): {
|
|
230
|
+
agent: T | undefined;
|
|
231
|
+
connection: Connection | undefined;
|
|
232
|
+
request: Request<unknown, CfProperties<unknown>> | undefined;
|
|
233
|
+
} {
|
|
234
|
+
const store = agentContext.getStore() as
|
|
235
|
+
| {
|
|
236
|
+
agent: T;
|
|
237
|
+
connection: Connection | undefined;
|
|
238
|
+
request: Request<unknown, CfProperties<unknown>> | undefined;
|
|
239
|
+
}
|
|
240
|
+
| undefined;
|
|
241
|
+
if (!store) {
|
|
242
|
+
return {
|
|
243
|
+
agent: undefined,
|
|
244
|
+
connection: undefined,
|
|
245
|
+
request: undefined,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return store;
|
|
249
|
+
}
|
|
250
|
+
|
|
179
251
|
/**
|
|
180
252
|
* Base class for creating Agent implementations
|
|
181
253
|
* @template Env Environment type containing bindings
|
|
182
254
|
* @template State State type to store within the Agent
|
|
183
255
|
*/
|
|
184
256
|
export class Agent<Env, State = unknown> extends Server<Env> {
|
|
185
|
-
|
|
257
|
+
private _state = DEFAULT_STATE as State;
|
|
258
|
+
|
|
259
|
+
private _ParentClass: typeof Agent<Env, State> =
|
|
260
|
+
Object.getPrototypeOf(this).constructor;
|
|
261
|
+
|
|
262
|
+
mcp: MCPClientManager = new MCPClientManager(this._ParentClass.name, "0.0.1");
|
|
186
263
|
|
|
187
264
|
/**
|
|
188
265
|
* Initial state for the Agent
|
|
@@ -194,9 +271,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
194
271
|
* Current state of the Agent
|
|
195
272
|
*/
|
|
196
273
|
get state(): State {
|
|
197
|
-
if (this
|
|
274
|
+
if (this._state !== DEFAULT_STATE) {
|
|
198
275
|
// state was previously set, and populated internal state
|
|
199
|
-
return this
|
|
276
|
+
return this._state;
|
|
200
277
|
}
|
|
201
278
|
// looks like this is the first time the state is being accessed
|
|
202
279
|
// check if the state was set in a previous life
|
|
@@ -216,8 +293,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
216
293
|
) {
|
|
217
294
|
const state = result[0]?.state as string; // could be null?
|
|
218
295
|
|
|
219
|
-
this
|
|
220
|
-
return this
|
|
296
|
+
this._state = JSON.parse(state);
|
|
297
|
+
return this._state;
|
|
221
298
|
}
|
|
222
299
|
|
|
223
300
|
// ok, this is the first time the state is being accessed
|
|
@@ -278,7 +355,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
278
355
|
`;
|
|
279
356
|
|
|
280
357
|
void this.ctx.blockConcurrencyWhile(async () => {
|
|
281
|
-
return this
|
|
358
|
+
return this._tryCatch(async () => {
|
|
282
359
|
// Create alarms table if it doesn't exist
|
|
283
360
|
this.sql`
|
|
284
361
|
CREATE TABLE IF NOT EXISTS cf_agents_schedules (
|
|
@@ -298,13 +375,53 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
298
375
|
});
|
|
299
376
|
});
|
|
300
377
|
|
|
378
|
+
this.sql`
|
|
379
|
+
CREATE TABLE IF NOT EXISTS cf_agents_mcp_servers (
|
|
380
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
381
|
+
name TEXT NOT NULL,
|
|
382
|
+
server_url TEXT NOT NULL,
|
|
383
|
+
callback_url TEXT NOT NULL,
|
|
384
|
+
client_id TEXT,
|
|
385
|
+
auth_url TEXT,
|
|
386
|
+
server_options TEXT
|
|
387
|
+
)
|
|
388
|
+
`;
|
|
389
|
+
|
|
390
|
+
const _onRequest = this.onRequest.bind(this);
|
|
391
|
+
this.onRequest = (request: Request) => {
|
|
392
|
+
return agentContext.run(
|
|
393
|
+
{ agent: this, connection: undefined, request },
|
|
394
|
+
async () => {
|
|
395
|
+
if (this.mcp.isCallbackRequest(request)) {
|
|
396
|
+
await this.mcp.handleCallbackRequest(request);
|
|
397
|
+
|
|
398
|
+
// after the MCP connection handshake, we can send updated mcp state
|
|
399
|
+
this.broadcast(
|
|
400
|
+
JSON.stringify({
|
|
401
|
+
type: "cf_agent_mcp_servers",
|
|
402
|
+
mcp: this.getMcpServers(),
|
|
403
|
+
})
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// We probably should let the user configure this response/redirect, but this is fine for now.
|
|
407
|
+
return new Response("<script>window.close();</script>", {
|
|
408
|
+
status: 200,
|
|
409
|
+
headers: { "content-type": "text/html" },
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return this._tryCatch(() => _onRequest(request));
|
|
414
|
+
}
|
|
415
|
+
);
|
|
416
|
+
};
|
|
417
|
+
|
|
301
418
|
const _onMessage = this.onMessage.bind(this);
|
|
302
419
|
this.onMessage = async (connection: Connection, message: WSMessage) => {
|
|
303
|
-
return
|
|
420
|
+
return agentContext.run(
|
|
304
421
|
{ agent: this, connection, request: undefined },
|
|
305
422
|
async () => {
|
|
306
423
|
if (typeof message !== "string") {
|
|
307
|
-
return this
|
|
424
|
+
return this._tryCatch(() => _onMessage(connection, message));
|
|
308
425
|
}
|
|
309
426
|
|
|
310
427
|
let parsed: unknown;
|
|
@@ -312,11 +429,11 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
312
429
|
parsed = JSON.parse(message);
|
|
313
430
|
} catch (e) {
|
|
314
431
|
// silently fail and let the onMessage handler handle it
|
|
315
|
-
return this
|
|
432
|
+
return this._tryCatch(() => _onMessage(connection, message));
|
|
316
433
|
}
|
|
317
434
|
|
|
318
435
|
if (isStateUpdateMessage(parsed)) {
|
|
319
|
-
this
|
|
436
|
+
this._setStateInternal(parsed.state as State, connection);
|
|
320
437
|
return;
|
|
321
438
|
}
|
|
322
439
|
|
|
@@ -330,11 +447,10 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
330
447
|
throw new Error(`Method ${method} does not exist`);
|
|
331
448
|
}
|
|
332
449
|
|
|
333
|
-
if (!this
|
|
450
|
+
if (!this._isCallable(method)) {
|
|
334
451
|
throw new Error(`Method ${method} is not callable`);
|
|
335
452
|
}
|
|
336
453
|
|
|
337
|
-
// biome-ignore lint/complexity/noBannedTypes: <explanation>
|
|
338
454
|
const metadata = callableMetadata.get(methodFn as Function);
|
|
339
455
|
|
|
340
456
|
// For streaming methods, pass a StreamingResponse object
|
|
@@ -369,7 +485,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
369
485
|
return;
|
|
370
486
|
}
|
|
371
487
|
|
|
372
|
-
return this
|
|
488
|
+
return this._tryCatch(() => _onMessage(connection, message));
|
|
373
489
|
}
|
|
374
490
|
);
|
|
375
491
|
};
|
|
@@ -378,7 +494,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
378
494
|
this.onConnect = (connection: Connection, ctx: ConnectionContext) => {
|
|
379
495
|
// TODO: This is a hack to ensure the state is sent after the connection is established
|
|
380
496
|
// must fix this
|
|
381
|
-
return
|
|
497
|
+
return agentContext.run(
|
|
382
498
|
{ agent: this, connection, request: ctx.request },
|
|
383
499
|
async () => {
|
|
384
500
|
setTimeout(() => {
|
|
@@ -390,15 +506,67 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
390
506
|
})
|
|
391
507
|
);
|
|
392
508
|
}
|
|
393
|
-
|
|
509
|
+
|
|
510
|
+
connection.send(
|
|
511
|
+
JSON.stringify({
|
|
512
|
+
type: "cf_agent_mcp_servers",
|
|
513
|
+
mcp: this.getMcpServers(),
|
|
514
|
+
})
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
return this._tryCatch(() => _onConnect(connection, ctx));
|
|
394
518
|
}, 20);
|
|
395
519
|
}
|
|
396
520
|
);
|
|
397
521
|
};
|
|
522
|
+
|
|
523
|
+
const _onStart = this.onStart.bind(this);
|
|
524
|
+
this.onStart = async () => {
|
|
525
|
+
return agentContext.run(
|
|
526
|
+
{ agent: this, connection: undefined, request: undefined },
|
|
527
|
+
async () => {
|
|
528
|
+
const servers = this.sql<MCPServerRow>`
|
|
529
|
+
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
530
|
+
`;
|
|
531
|
+
|
|
532
|
+
// from DO storage, reconnect to all servers not currently in the oauth flow using our saved auth information
|
|
533
|
+
await Promise.allSettled(
|
|
534
|
+
servers
|
|
535
|
+
.filter((server) => server.auth_url === null)
|
|
536
|
+
.map((server) => {
|
|
537
|
+
return this._connectToMcpServerInternal(
|
|
538
|
+
server.name,
|
|
539
|
+
server.server_url,
|
|
540
|
+
server.callback_url,
|
|
541
|
+
server.server_options
|
|
542
|
+
? JSON.parse(server.server_options)
|
|
543
|
+
: undefined,
|
|
544
|
+
{
|
|
545
|
+
id: server.id,
|
|
546
|
+
oauthClientId: server.client_id ?? undefined,
|
|
547
|
+
}
|
|
548
|
+
);
|
|
549
|
+
})
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
this.broadcast(
|
|
553
|
+
JSON.stringify({
|
|
554
|
+
type: "cf_agent_mcp_servers",
|
|
555
|
+
mcp: this.getMcpServers(),
|
|
556
|
+
})
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
await this._tryCatch(() => _onStart());
|
|
560
|
+
}
|
|
561
|
+
);
|
|
562
|
+
};
|
|
398
563
|
}
|
|
399
564
|
|
|
400
|
-
|
|
401
|
-
|
|
565
|
+
private _setStateInternal(
|
|
566
|
+
state: State,
|
|
567
|
+
source: Connection | "server" = "server"
|
|
568
|
+
) {
|
|
569
|
+
this._state = state;
|
|
402
570
|
this.sql`
|
|
403
571
|
INSERT OR REPLACE INTO cf_agents_state (id, state)
|
|
404
572
|
VALUES (${STATE_ROW_ID}, ${JSON.stringify(state)})
|
|
@@ -414,9 +582,9 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
414
582
|
}),
|
|
415
583
|
source !== "server" ? [source.id] : []
|
|
416
584
|
);
|
|
417
|
-
return this
|
|
418
|
-
const { connection, request } =
|
|
419
|
-
return
|
|
585
|
+
return this._tryCatch(() => {
|
|
586
|
+
const { connection, request } = agentContext.getStore() || {};
|
|
587
|
+
return agentContext.run(
|
|
420
588
|
{ agent: this, connection, request },
|
|
421
589
|
async () => {
|
|
422
590
|
return this.onStateUpdate(state, source);
|
|
@@ -430,7 +598,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
430
598
|
* @param state New state to set
|
|
431
599
|
*/
|
|
432
600
|
setState(state: State) {
|
|
433
|
-
this
|
|
601
|
+
this._setStateInternal(state, "server");
|
|
434
602
|
}
|
|
435
603
|
|
|
436
604
|
/**
|
|
@@ -447,7 +615,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
447
615
|
* @param email Email message to process
|
|
448
616
|
*/
|
|
449
617
|
onEmail(email: ForwardableEmailMessage) {
|
|
450
|
-
return
|
|
618
|
+
return agentContext.run(
|
|
451
619
|
{ agent: this, connection: undefined, request: undefined },
|
|
452
620
|
async () => {
|
|
453
621
|
console.error("onEmail not implemented");
|
|
@@ -455,7 +623,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
455
623
|
);
|
|
456
624
|
}
|
|
457
625
|
|
|
458
|
-
async
|
|
626
|
+
private async _tryCatch<T>(fn: () => T | Promise<T>) {
|
|
459
627
|
try {
|
|
460
628
|
return await fn();
|
|
461
629
|
} catch (e) {
|
|
@@ -529,7 +697,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
529
697
|
)}, 'scheduled', ${timestamp})
|
|
530
698
|
`;
|
|
531
699
|
|
|
532
|
-
await this
|
|
700
|
+
await this._scheduleNextAlarm();
|
|
533
701
|
|
|
534
702
|
return {
|
|
535
703
|
id,
|
|
@@ -550,7 +718,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
550
718
|
)}, 'delayed', ${when}, ${timestamp})
|
|
551
719
|
`;
|
|
552
720
|
|
|
553
|
-
await this
|
|
721
|
+
await this._scheduleNextAlarm();
|
|
554
722
|
|
|
555
723
|
return {
|
|
556
724
|
id,
|
|
@@ -572,7 +740,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
572
740
|
)}, 'cron', ${when}, ${timestamp})
|
|
573
741
|
`;
|
|
574
742
|
|
|
575
|
-
await this
|
|
743
|
+
await this._scheduleNextAlarm();
|
|
576
744
|
|
|
577
745
|
return {
|
|
578
746
|
id,
|
|
@@ -659,11 +827,11 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
659
827
|
async cancelSchedule(id: string): Promise<boolean> {
|
|
660
828
|
this.sql`DELETE FROM cf_agents_schedules WHERE id = ${id}`;
|
|
661
829
|
|
|
662
|
-
await this
|
|
830
|
+
await this._scheduleNextAlarm();
|
|
663
831
|
return true;
|
|
664
832
|
}
|
|
665
833
|
|
|
666
|
-
async
|
|
834
|
+
private async _scheduleNextAlarm() {
|
|
667
835
|
// Find the next schedule that needs to be executed
|
|
668
836
|
const result = this.sql`
|
|
669
837
|
SELECT time FROM cf_agents_schedules
|
|
@@ -680,10 +848,14 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
680
848
|
}
|
|
681
849
|
|
|
682
850
|
/**
|
|
683
|
-
* Method called when an alarm fires
|
|
684
|
-
* Executes any scheduled tasks that are due
|
|
851
|
+
* Method called when an alarm fires.
|
|
852
|
+
* Executes any scheduled tasks that are due.
|
|
853
|
+
*
|
|
854
|
+
* @remarks
|
|
855
|
+
* To schedule a task, please use the `this.schedule` method instead.
|
|
856
|
+
* See {@link https://developers.cloudflare.com/agents/api-reference/schedule-tasks/}
|
|
685
857
|
*/
|
|
686
|
-
async
|
|
858
|
+
public readonly alarm = async () => {
|
|
687
859
|
const now = Math.floor(Date.now() / 1000);
|
|
688
860
|
|
|
689
861
|
// Get all schedules that should be executed now
|
|
@@ -697,7 +869,7 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
697
869
|
console.error(`callback ${row.callback} not found`);
|
|
698
870
|
continue;
|
|
699
871
|
}
|
|
700
|
-
await
|
|
872
|
+
await agentContext.run(
|
|
701
873
|
{ agent: this, connection: undefined, request: undefined },
|
|
702
874
|
async () => {
|
|
703
875
|
try {
|
|
@@ -729,8 +901,8 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
729
901
|
}
|
|
730
902
|
|
|
731
903
|
// Schedule the next alarm
|
|
732
|
-
await this
|
|
733
|
-
}
|
|
904
|
+
await this._scheduleNextAlarm();
|
|
905
|
+
};
|
|
734
906
|
|
|
735
907
|
/**
|
|
736
908
|
* Destroy the Agent, removing all state and scheduled tasks
|
|
@@ -739,20 +911,184 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
739
911
|
// drop all tables
|
|
740
912
|
this.sql`DROP TABLE IF EXISTS cf_agents_state`;
|
|
741
913
|
this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
|
|
914
|
+
this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
|
|
742
915
|
|
|
743
916
|
// delete all alarms
|
|
744
917
|
await this.ctx.storage.deleteAlarm();
|
|
745
918
|
await this.ctx.storage.deleteAll();
|
|
919
|
+
this.ctx.abort("destroyed"); // enforce that the agent is evicted
|
|
746
920
|
}
|
|
747
921
|
|
|
748
922
|
/**
|
|
749
923
|
* Get all methods marked as callable on this Agent
|
|
750
924
|
* @returns A map of method names to their metadata
|
|
751
925
|
*/
|
|
752
|
-
|
|
753
|
-
// biome-ignore lint/complexity/noBannedTypes: <explanation>
|
|
926
|
+
private _isCallable(method: string): boolean {
|
|
754
927
|
return callableMetadata.has(this[method as keyof this] as Function);
|
|
755
928
|
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Connect to a new MCP Server
|
|
932
|
+
*
|
|
933
|
+
* @param url MCP Server SSE URL
|
|
934
|
+
* @param callbackHost Base host for the agent, used for the redirect URI.
|
|
935
|
+
* @param agentsPrefix agents routing prefix if not using `agents`
|
|
936
|
+
* @param options MCP client and transport (header) options
|
|
937
|
+
* @returns authUrl
|
|
938
|
+
*/
|
|
939
|
+
async addMcpServer(
|
|
940
|
+
serverName: string,
|
|
941
|
+
url: string,
|
|
942
|
+
callbackHost: string,
|
|
943
|
+
agentsPrefix = "agents",
|
|
944
|
+
options?: {
|
|
945
|
+
client?: ConstructorParameters<typeof Client>[1];
|
|
946
|
+
transport?: {
|
|
947
|
+
headers: HeadersInit;
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
): Promise<{ id: string; authUrl: string | undefined }> {
|
|
951
|
+
const callbackUrl = `${callbackHost}/${agentsPrefix}/${camelCaseToKebabCase(this._ParentClass.name)}/${this.name}/callback`;
|
|
952
|
+
|
|
953
|
+
const result = await this._connectToMcpServerInternal(
|
|
954
|
+
serverName,
|
|
955
|
+
url,
|
|
956
|
+
callbackUrl,
|
|
957
|
+
options
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
this.broadcast(
|
|
961
|
+
JSON.stringify({
|
|
962
|
+
type: "cf_agent_mcp_servers",
|
|
963
|
+
mcp: this.getMcpServers(),
|
|
964
|
+
})
|
|
965
|
+
);
|
|
966
|
+
|
|
967
|
+
return result;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async _connectToMcpServerInternal(
|
|
971
|
+
serverName: string,
|
|
972
|
+
url: string,
|
|
973
|
+
callbackUrl: string,
|
|
974
|
+
// it's important that any options here are serializable because we put them into our sqlite DB for reconnection purposes
|
|
975
|
+
options?: {
|
|
976
|
+
client?: ConstructorParameters<typeof Client>[1];
|
|
977
|
+
/**
|
|
978
|
+
* We don't expose the normal set of transport options because:
|
|
979
|
+
* 1) we can't serialize things like the auth provider or a fetch function into the DB for reconnection purposes
|
|
980
|
+
* 2) We probably want these options to be agnostic to the transport type (SSE vs Streamable)
|
|
981
|
+
*
|
|
982
|
+
* 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).
|
|
983
|
+
*/
|
|
984
|
+
transport?: {
|
|
985
|
+
headers?: HeadersInit;
|
|
986
|
+
};
|
|
987
|
+
},
|
|
988
|
+
reconnect?: {
|
|
989
|
+
id: string;
|
|
990
|
+
oauthClientId?: string;
|
|
991
|
+
}
|
|
992
|
+
): Promise<{ id: string; authUrl: string | undefined }> {
|
|
993
|
+
const authProvider = new DurableObjectOAuthClientProvider(
|
|
994
|
+
this.ctx.storage,
|
|
995
|
+
this.name,
|
|
996
|
+
callbackUrl
|
|
997
|
+
);
|
|
998
|
+
|
|
999
|
+
if (reconnect) {
|
|
1000
|
+
authProvider.serverId = reconnect.id;
|
|
1001
|
+
if (reconnect.oauthClientId) {
|
|
1002
|
+
authProvider.clientId = reconnect.oauthClientId;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// allows passing through transport headers if necessary
|
|
1007
|
+
// this handles some non-standard bearer auth setups (i.e. MCP server behind CF access instead of OAuth)
|
|
1008
|
+
let headerTransportOpts: SSEClientTransportOptions = {};
|
|
1009
|
+
if (options?.transport?.headers) {
|
|
1010
|
+
headerTransportOpts = {
|
|
1011
|
+
eventSourceInit: {
|
|
1012
|
+
fetch: (url, init) =>
|
|
1013
|
+
fetch(url, {
|
|
1014
|
+
...init,
|
|
1015
|
+
headers: options?.transport?.headers,
|
|
1016
|
+
}),
|
|
1017
|
+
},
|
|
1018
|
+
requestInit: {
|
|
1019
|
+
headers: options?.transport?.headers,
|
|
1020
|
+
},
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const { id, authUrl, clientId } = await this.mcp.connect(url, {
|
|
1025
|
+
reconnect,
|
|
1026
|
+
transport: {
|
|
1027
|
+
...headerTransportOpts,
|
|
1028
|
+
authProvider,
|
|
1029
|
+
},
|
|
1030
|
+
client: options?.client,
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
this.sql`
|
|
1034
|
+
INSERT OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
|
|
1035
|
+
VALUES (
|
|
1036
|
+
${id},
|
|
1037
|
+
${serverName},
|
|
1038
|
+
${url},
|
|
1039
|
+
${clientId ?? null},
|
|
1040
|
+
${authUrl ?? null},
|
|
1041
|
+
${callbackUrl},
|
|
1042
|
+
${options ? JSON.stringify(options) : null}
|
|
1043
|
+
);
|
|
1044
|
+
`;
|
|
1045
|
+
|
|
1046
|
+
return {
|
|
1047
|
+
id,
|
|
1048
|
+
authUrl,
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
async removeMcpServer(id: string) {
|
|
1053
|
+
this.mcp.closeConnection(id);
|
|
1054
|
+
this.sql`
|
|
1055
|
+
DELETE FROM cf_agents_mcp_servers WHERE id = ${id};
|
|
1056
|
+
`;
|
|
1057
|
+
this.broadcast(
|
|
1058
|
+
JSON.stringify({
|
|
1059
|
+
type: "cf_agent_mcp_servers",
|
|
1060
|
+
mcp: this.getMcpServers(),
|
|
1061
|
+
})
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
getMcpServers(): MCPServersState {
|
|
1066
|
+
const mcpState: MCPServersState = {
|
|
1067
|
+
servers: {},
|
|
1068
|
+
tools: this.mcp.listTools(),
|
|
1069
|
+
prompts: this.mcp.listPrompts(),
|
|
1070
|
+
resources: this.mcp.listResources(),
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
const servers = this.sql<MCPServerRow>`
|
|
1074
|
+
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
1075
|
+
`;
|
|
1076
|
+
|
|
1077
|
+
for (const server of servers) {
|
|
1078
|
+
const serverConn = this.mcp.mcpConnections[server.id];
|
|
1079
|
+
mcpState.servers[server.id] = {
|
|
1080
|
+
name: server.name,
|
|
1081
|
+
server_url: server.server_url,
|
|
1082
|
+
auth_url: server.auth_url,
|
|
1083
|
+
// mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
|
|
1084
|
+
state: serverConn?.connectionState ?? "authenticating",
|
|
1085
|
+
instructions: serverConn?.instructions ?? null,
|
|
1086
|
+
capabilities: serverConn?.serverCapabilities ?? null,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
return mcpState;
|
|
1091
|
+
}
|
|
756
1092
|
}
|
|
757
1093
|
|
|
758
1094
|
/**
|
|
@@ -856,7 +1192,7 @@ export async function routeAgentEmail<Env>(
|
|
|
856
1192
|
* @param options Options for Agent creation
|
|
857
1193
|
* @returns Promise resolving to an Agent instance stub
|
|
858
1194
|
*/
|
|
859
|
-
export function getAgentByName<Env, T extends Agent<Env>>(
|
|
1195
|
+
export async function getAgentByName<Env, T extends Agent<Env>>(
|
|
860
1196
|
namespace: AgentNamespace<T>,
|
|
861
1197
|
name: string,
|
|
862
1198
|
options?: {
|
|
@@ -871,13 +1207,13 @@ export function getAgentByName<Env, T extends Agent<Env>>(
|
|
|
871
1207
|
* A wrapper for streaming responses in callable methods
|
|
872
1208
|
*/
|
|
873
1209
|
export class StreamingResponse {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1210
|
+
private _connection: Connection;
|
|
1211
|
+
private _id: string;
|
|
1212
|
+
private _closed = false;
|
|
877
1213
|
|
|
878
1214
|
constructor(connection: Connection, id: string) {
|
|
879
|
-
this
|
|
880
|
-
this
|
|
1215
|
+
this._connection = connection;
|
|
1216
|
+
this._id = id;
|
|
881
1217
|
}
|
|
882
1218
|
|
|
883
1219
|
/**
|
|
@@ -885,17 +1221,17 @@ export class StreamingResponse {
|
|
|
885
1221
|
* @param chunk The data to send
|
|
886
1222
|
*/
|
|
887
1223
|
send(chunk: unknown) {
|
|
888
|
-
if (this
|
|
1224
|
+
if (this._closed) {
|
|
889
1225
|
throw new Error("StreamingResponse is already closed");
|
|
890
1226
|
}
|
|
891
1227
|
const response: RPCResponse = {
|
|
892
1228
|
type: "rpc",
|
|
893
|
-
id: this
|
|
1229
|
+
id: this._id,
|
|
894
1230
|
success: true,
|
|
895
1231
|
result: chunk,
|
|
896
1232
|
done: false,
|
|
897
1233
|
};
|
|
898
|
-
this
|
|
1234
|
+
this._connection.send(JSON.stringify(response));
|
|
899
1235
|
}
|
|
900
1236
|
|
|
901
1237
|
/**
|
|
@@ -903,17 +1239,17 @@ export class StreamingResponse {
|
|
|
903
1239
|
* @param finalChunk Optional final chunk of data to send
|
|
904
1240
|
*/
|
|
905
1241
|
end(finalChunk?: unknown) {
|
|
906
|
-
if (this
|
|
1242
|
+
if (this._closed) {
|
|
907
1243
|
throw new Error("StreamingResponse is already closed");
|
|
908
1244
|
}
|
|
909
|
-
this
|
|
1245
|
+
this._closed = true;
|
|
910
1246
|
const response: RPCResponse = {
|
|
911
1247
|
type: "rpc",
|
|
912
|
-
id: this
|
|
1248
|
+
id: this._id,
|
|
913
1249
|
success: true,
|
|
914
1250
|
result: finalChunk,
|
|
915
1251
|
done: true,
|
|
916
1252
|
};
|
|
917
|
-
this
|
|
1253
|
+
this._connection.send(JSON.stringify(response));
|
|
918
1254
|
}
|
|
919
1255
|
}
|