agents 0.0.0-88ea3a1 → 0.0.0-89b649a
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 +49 -4
- package/dist/ai-chat-agent.js +129 -66
- package/dist/ai-chat-agent.js.map +1 -1
- package/dist/ai-react.d.ts +13 -0
- package/dist/ai-react.js +38 -24
- package/dist/ai-react.js.map +1 -1
- package/dist/ai-types.d.ts +5 -0
- package/dist/chunk-BZXOAZUX.js +106 -0
- package/dist/chunk-BZXOAZUX.js.map +1 -0
- package/dist/{chunk-YMUU7QHV.js → chunk-MXJNY43J.js} +330 -140
- package/dist/chunk-MXJNY43J.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 +15 -1
- package/dist/client.js +6 -126
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +120 -17
- package/dist/index.js +8 -8
- package/dist/mcp/client.d.ts +142 -34
- package/dist/mcp/client.js +3 -262
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/do-oauth-client-provider.d.ts +41 -0
- package/dist/mcp/do-oauth-client-provider.js +7 -0
- package/dist/mcp/index.d.ts +44 -5
- package/dist/mcp/index.js +613 -174
- package/dist/mcp/index.js.map +1 -1
- package/dist/react.d.ts +85 -5
- package/dist/react.js +34 -27
- package/dist/react.js.map +1 -1
- package/dist/schedule.js +0 -2
- 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 +30 -5
- package/src/index.ts +395 -60
- package/dist/chunk-HMLY7DHA.js +0 -16
- package/dist/chunk-YMUU7QHV.js.map +0 -1
- /package/dist/{chunk-HMLY7DHA.js.map → mcp/do-oauth-client-provider.js.map} +0 -0
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,6 +911,7 @@ 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();
|
|
@@ -749,10 +922,172 @@ export class Agent<Env, State = unknown> extends Server<Env> {
|
|
|
749
922
|
* Get all methods marked as callable on this Agent
|
|
750
923
|
* @returns A map of method names to their metadata
|
|
751
924
|
*/
|
|
752
|
-
|
|
753
|
-
// biome-ignore lint/complexity/noBannedTypes: <explanation>
|
|
925
|
+
private _isCallable(method: string): boolean {
|
|
754
926
|
return callableMetadata.has(this[method as keyof this] as Function);
|
|
755
927
|
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Connect to a new MCP Server
|
|
931
|
+
*
|
|
932
|
+
* @param url MCP Server SSE URL
|
|
933
|
+
* @param callbackHost Base host for the agent, used for the redirect URI.
|
|
934
|
+
* @param agentsPrefix agents routing prefix if not using `agents`
|
|
935
|
+
* @param options MCP client and transport (header) options
|
|
936
|
+
* @returns authUrl
|
|
937
|
+
*/
|
|
938
|
+
async addMcpServer(
|
|
939
|
+
serverName: string,
|
|
940
|
+
url: string,
|
|
941
|
+
callbackHost: string,
|
|
942
|
+
agentsPrefix = "agents",
|
|
943
|
+
options?: {
|
|
944
|
+
client?: ConstructorParameters<typeof Client>[1];
|
|
945
|
+
transport?: {
|
|
946
|
+
headers: HeadersInit;
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
): Promise<{ id: string; authUrl: string | undefined }> {
|
|
950
|
+
const callbackUrl = `${callbackHost}/${agentsPrefix}/${camelCaseToKebabCase(this._ParentClass.name)}/${this.name}/callback`;
|
|
951
|
+
|
|
952
|
+
const result = await this._connectToMcpServerInternal(
|
|
953
|
+
serverName,
|
|
954
|
+
url,
|
|
955
|
+
callbackUrl,
|
|
956
|
+
options
|
|
957
|
+
);
|
|
958
|
+
|
|
959
|
+
this.broadcast(
|
|
960
|
+
JSON.stringify({
|
|
961
|
+
type: "cf_agent_mcp_servers",
|
|
962
|
+
mcp: this.getMcpServers(),
|
|
963
|
+
})
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
return result;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async _connectToMcpServerInternal(
|
|
970
|
+
serverName: string,
|
|
971
|
+
url: string,
|
|
972
|
+
callbackUrl: string,
|
|
973
|
+
// it's important that any options here are serializable because we put them into our sqlite DB for reconnection purposes
|
|
974
|
+
options?: {
|
|
975
|
+
client?: ConstructorParameters<typeof Client>[1];
|
|
976
|
+
/**
|
|
977
|
+
* We don't expose the normal set of transport options because:
|
|
978
|
+
* 1) we can't serialize things like the auth provider or a fetch function into the DB for reconnection purposes
|
|
979
|
+
* 2) We probably want these options to be agnostic to the transport type (SSE vs Streamable)
|
|
980
|
+
*
|
|
981
|
+
* 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).
|
|
982
|
+
*/
|
|
983
|
+
transport?: {
|
|
984
|
+
headers?: HeadersInit;
|
|
985
|
+
};
|
|
986
|
+
},
|
|
987
|
+
reconnect?: {
|
|
988
|
+
id: string;
|
|
989
|
+
oauthClientId?: string;
|
|
990
|
+
}
|
|
991
|
+
): Promise<{ id: string; authUrl: string | undefined }> {
|
|
992
|
+
const authProvider = new DurableObjectOAuthClientProvider(
|
|
993
|
+
this.ctx.storage,
|
|
994
|
+
this.name,
|
|
995
|
+
callbackUrl
|
|
996
|
+
);
|
|
997
|
+
|
|
998
|
+
if (reconnect) {
|
|
999
|
+
authProvider.serverId = reconnect.id;
|
|
1000
|
+
if (reconnect.oauthClientId) {
|
|
1001
|
+
authProvider.clientId = reconnect.oauthClientId;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// allows passing through transport headers if necessary
|
|
1006
|
+
// this handles some non-standard bearer auth setups (i.e. MCP server behind CF access instead of OAuth)
|
|
1007
|
+
let headerTransportOpts: SSEClientTransportOptions = {};
|
|
1008
|
+
if (options?.transport?.headers) {
|
|
1009
|
+
headerTransportOpts = {
|
|
1010
|
+
eventSourceInit: {
|
|
1011
|
+
fetch: (url, init) =>
|
|
1012
|
+
fetch(url, {
|
|
1013
|
+
...init,
|
|
1014
|
+
headers: options?.transport?.headers,
|
|
1015
|
+
}),
|
|
1016
|
+
},
|
|
1017
|
+
requestInit: {
|
|
1018
|
+
headers: options?.transport?.headers,
|
|
1019
|
+
},
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const { id, authUrl, clientId } = await this.mcp.connect(url, {
|
|
1024
|
+
reconnect,
|
|
1025
|
+
transport: {
|
|
1026
|
+
...headerTransportOpts,
|
|
1027
|
+
authProvider,
|
|
1028
|
+
},
|
|
1029
|
+
client: options?.client,
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
this.sql`
|
|
1033
|
+
INSERT OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
|
|
1034
|
+
VALUES (
|
|
1035
|
+
${id},
|
|
1036
|
+
${serverName},
|
|
1037
|
+
${url},
|
|
1038
|
+
${clientId ?? null},
|
|
1039
|
+
${authUrl ?? null},
|
|
1040
|
+
${callbackUrl},
|
|
1041
|
+
${options ? JSON.stringify(options) : null}
|
|
1042
|
+
);
|
|
1043
|
+
`;
|
|
1044
|
+
|
|
1045
|
+
return {
|
|
1046
|
+
id,
|
|
1047
|
+
authUrl,
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
async removeMcpServer(id: string) {
|
|
1052
|
+
this.mcp.closeConnection(id);
|
|
1053
|
+
this.sql`
|
|
1054
|
+
DELETE FROM cf_agents_mcp_servers WHERE id = ${id};
|
|
1055
|
+
`;
|
|
1056
|
+
this.broadcast(
|
|
1057
|
+
JSON.stringify({
|
|
1058
|
+
type: "cf_agent_mcp_servers",
|
|
1059
|
+
mcp: this.getMcpServers(),
|
|
1060
|
+
})
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
getMcpServers(): MCPServersState {
|
|
1065
|
+
const mcpState: MCPServersState = {
|
|
1066
|
+
servers: {},
|
|
1067
|
+
tools: this.mcp.listTools(),
|
|
1068
|
+
prompts: this.mcp.listPrompts(),
|
|
1069
|
+
resources: this.mcp.listResources(),
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
const servers = this.sql<MCPServerRow>`
|
|
1073
|
+
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
1074
|
+
`;
|
|
1075
|
+
|
|
1076
|
+
for (const server of servers) {
|
|
1077
|
+
const serverConn = this.mcp.mcpConnections[server.id];
|
|
1078
|
+
mcpState.servers[server.id] = {
|
|
1079
|
+
name: server.name,
|
|
1080
|
+
server_url: server.server_url,
|
|
1081
|
+
auth_url: server.auth_url,
|
|
1082
|
+
// mark as "authenticating" because the server isn't automatically connected, so it's pending authenticating
|
|
1083
|
+
state: serverConn?.connectionState ?? "authenticating",
|
|
1084
|
+
instructions: serverConn?.instructions ?? null,
|
|
1085
|
+
capabilities: serverConn?.serverCapabilities ?? null,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return mcpState;
|
|
1090
|
+
}
|
|
756
1091
|
}
|
|
757
1092
|
|
|
758
1093
|
/**
|
|
@@ -856,7 +1191,7 @@ export async function routeAgentEmail<Env>(
|
|
|
856
1191
|
* @param options Options for Agent creation
|
|
857
1192
|
* @returns Promise resolving to an Agent instance stub
|
|
858
1193
|
*/
|
|
859
|
-
export function getAgentByName<Env, T extends Agent<Env>>(
|
|
1194
|
+
export async function getAgentByName<Env, T extends Agent<Env>>(
|
|
860
1195
|
namespace: AgentNamespace<T>,
|
|
861
1196
|
name: string,
|
|
862
1197
|
options?: {
|
|
@@ -871,13 +1206,13 @@ export function getAgentByName<Env, T extends Agent<Env>>(
|
|
|
871
1206
|
* A wrapper for streaming responses in callable methods
|
|
872
1207
|
*/
|
|
873
1208
|
export class StreamingResponse {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1209
|
+
private _connection: Connection;
|
|
1210
|
+
private _id: string;
|
|
1211
|
+
private _closed = false;
|
|
877
1212
|
|
|
878
1213
|
constructor(connection: Connection, id: string) {
|
|
879
|
-
this
|
|
880
|
-
this
|
|
1214
|
+
this._connection = connection;
|
|
1215
|
+
this._id = id;
|
|
881
1216
|
}
|
|
882
1217
|
|
|
883
1218
|
/**
|
|
@@ -885,17 +1220,17 @@ export class StreamingResponse {
|
|
|
885
1220
|
* @param chunk The data to send
|
|
886
1221
|
*/
|
|
887
1222
|
send(chunk: unknown) {
|
|
888
|
-
if (this
|
|
1223
|
+
if (this._closed) {
|
|
889
1224
|
throw new Error("StreamingResponse is already closed");
|
|
890
1225
|
}
|
|
891
1226
|
const response: RPCResponse = {
|
|
892
1227
|
type: "rpc",
|
|
893
|
-
id: this
|
|
1228
|
+
id: this._id,
|
|
894
1229
|
success: true,
|
|
895
1230
|
result: chunk,
|
|
896
1231
|
done: false,
|
|
897
1232
|
};
|
|
898
|
-
this
|
|
1233
|
+
this._connection.send(JSON.stringify(response));
|
|
899
1234
|
}
|
|
900
1235
|
|
|
901
1236
|
/**
|
|
@@ -903,17 +1238,17 @@ export class StreamingResponse {
|
|
|
903
1238
|
* @param finalChunk Optional final chunk of data to send
|
|
904
1239
|
*/
|
|
905
1240
|
end(finalChunk?: unknown) {
|
|
906
|
-
if (this
|
|
1241
|
+
if (this._closed) {
|
|
907
1242
|
throw new Error("StreamingResponse is already closed");
|
|
908
1243
|
}
|
|
909
|
-
this
|
|
1244
|
+
this._closed = true;
|
|
910
1245
|
const response: RPCResponse = {
|
|
911
1246
|
type: "rpc",
|
|
912
|
-
id: this
|
|
1247
|
+
id: this._id,
|
|
913
1248
|
success: true,
|
|
914
1249
|
result: finalChunk,
|
|
915
1250
|
done: true,
|
|
916
1251
|
};
|
|
917
|
-
this
|
|
1252
|
+
this._connection.send(JSON.stringify(response));
|
|
918
1253
|
}
|
|
919
1254
|
}
|