hono-agents 0.0.0-ff431ff → 0.0.0-ff45307
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/index.js +738 -460
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import "partysocket";
|
|
2
2
|
import { nanoid } from "nanoid";
|
|
3
|
+
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker-provider.js";
|
|
3
4
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
|
-
import { ElicitRequestSchema, PromptListChangedNotificationSchema, ResourceListChangedNotificationSchema, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
6
6
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
7
|
+
import { ElicitRequestSchema, PromptListChangedNotificationSchema, ResourceListChangedNotificationSchema, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
7
8
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
9
|
import { parseCronExpression } from "cron-schedule";
|
|
9
10
|
import "cloudflare:email";
|
|
@@ -11,7 +12,7 @@ import { Server, routePartykitRequest } from "partyserver";
|
|
|
11
12
|
import { env } from "hono/adapter";
|
|
12
13
|
import { createMiddleware } from "hono/factory";
|
|
13
14
|
|
|
14
|
-
//#region ../agents/dist/ai-types-
|
|
15
|
+
//#region ../agents/dist/ai-types-CrMqkwc_.js
|
|
15
16
|
/**
|
|
16
17
|
* Enum for message types to improve type safety and maintainability
|
|
17
18
|
*/
|
|
@@ -21,6 +22,10 @@ let MessageType = /* @__PURE__ */ function(MessageType$1) {
|
|
|
21
22
|
MessageType$1["CF_AGENT_USE_CHAT_RESPONSE"] = "cf_agent_use_chat_response";
|
|
22
23
|
MessageType$1["CF_AGENT_CHAT_CLEAR"] = "cf_agent_chat_clear";
|
|
23
24
|
MessageType$1["CF_AGENT_CHAT_REQUEST_CANCEL"] = "cf_agent_chat_request_cancel";
|
|
25
|
+
/** Sent by server when client connects and there's an active stream to resume */
|
|
26
|
+
MessageType$1["CF_AGENT_STREAM_RESUMING"] = "cf_agent_stream_resuming";
|
|
27
|
+
/** Sent by client to acknowledge stream resuming notification and request chunks */
|
|
28
|
+
MessageType$1["CF_AGENT_STREAM_RESUME_ACK"] = "cf_agent_stream_resume_ack";
|
|
24
29
|
MessageType$1["CF_AGENT_MCP_SERVERS"] = "cf_agent_mcp_servers";
|
|
25
30
|
MessageType$1["CF_MCP_AGENT_EVENT"] = "cf_mcp_agent_event";
|
|
26
31
|
MessageType$1["CF_AGENT_STATE"] = "cf_agent_state";
|
|
@@ -29,7 +34,7 @@ let MessageType = /* @__PURE__ */ function(MessageType$1) {
|
|
|
29
34
|
}({});
|
|
30
35
|
|
|
31
36
|
//#endregion
|
|
32
|
-
//#region ../agents/dist/client-
|
|
37
|
+
//#region ../agents/dist/client-B3SR12TQ.js
|
|
33
38
|
/**
|
|
34
39
|
* Convert a camelCase string to a kebab-case string
|
|
35
40
|
* @param str The string to convert
|
|
@@ -43,7 +48,97 @@ function camelCaseToKebabCase(str) {
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
//#endregion
|
|
46
|
-
//#region ../agents/dist/client-
|
|
51
|
+
//#region ../agents/dist/do-oauth-client-provider-CwqK5SXm.js
|
|
52
|
+
var DurableObjectOAuthClientProvider = class {
|
|
53
|
+
constructor(storage, clientName, baseRedirectUrl) {
|
|
54
|
+
this.storage = storage;
|
|
55
|
+
this.clientName = clientName;
|
|
56
|
+
this.baseRedirectUrl = baseRedirectUrl;
|
|
57
|
+
if (!storage) throw new Error("DurableObjectOAuthClientProvider requires a valid DurableObjectStorage instance");
|
|
58
|
+
}
|
|
59
|
+
get clientMetadata() {
|
|
60
|
+
return {
|
|
61
|
+
client_name: this.clientName,
|
|
62
|
+
client_uri: this.clientUri,
|
|
63
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
64
|
+
redirect_uris: [this.redirectUrl],
|
|
65
|
+
response_types: ["code"],
|
|
66
|
+
token_endpoint_auth_method: "none"
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
get clientUri() {
|
|
70
|
+
return new URL(this.redirectUrl).origin;
|
|
71
|
+
}
|
|
72
|
+
get redirectUrl() {
|
|
73
|
+
return `${this.baseRedirectUrl}/${this.serverId}`;
|
|
74
|
+
}
|
|
75
|
+
get clientId() {
|
|
76
|
+
if (!this._clientId_) throw new Error("Trying to access clientId before it was set");
|
|
77
|
+
return this._clientId_;
|
|
78
|
+
}
|
|
79
|
+
set clientId(clientId_) {
|
|
80
|
+
this._clientId_ = clientId_;
|
|
81
|
+
}
|
|
82
|
+
get serverId() {
|
|
83
|
+
if (!this._serverId_) throw new Error("Trying to access serverId before it was set");
|
|
84
|
+
return this._serverId_;
|
|
85
|
+
}
|
|
86
|
+
set serverId(serverId_) {
|
|
87
|
+
this._serverId_ = serverId_;
|
|
88
|
+
}
|
|
89
|
+
keyPrefix(clientId) {
|
|
90
|
+
return `/${this.clientName}/${this.serverId}/${clientId}`;
|
|
91
|
+
}
|
|
92
|
+
clientInfoKey(clientId) {
|
|
93
|
+
return `${this.keyPrefix(clientId)}/client_info/`;
|
|
94
|
+
}
|
|
95
|
+
async clientInformation() {
|
|
96
|
+
if (!this._clientId_) return;
|
|
97
|
+
return await this.storage.get(this.clientInfoKey(this.clientId)) ?? void 0;
|
|
98
|
+
}
|
|
99
|
+
async saveClientInformation(clientInformation) {
|
|
100
|
+
await this.storage.put(this.clientInfoKey(clientInformation.client_id), clientInformation);
|
|
101
|
+
this.clientId = clientInformation.client_id;
|
|
102
|
+
}
|
|
103
|
+
tokenKey(clientId) {
|
|
104
|
+
return `${this.keyPrefix(clientId)}/token`;
|
|
105
|
+
}
|
|
106
|
+
async tokens() {
|
|
107
|
+
if (!this._clientId_) return;
|
|
108
|
+
return await this.storage.get(this.tokenKey(this.clientId)) ?? void 0;
|
|
109
|
+
}
|
|
110
|
+
async saveTokens(tokens) {
|
|
111
|
+
await this.storage.put(this.tokenKey(this.clientId), tokens);
|
|
112
|
+
}
|
|
113
|
+
get authUrl() {
|
|
114
|
+
return this._authUrl_;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Because this operates on the server side (but we need browser auth), we send this url back to the user
|
|
118
|
+
* and require user interact to initiate the redirect flow
|
|
119
|
+
*/
|
|
120
|
+
async redirectToAuthorization(authUrl) {
|
|
121
|
+
const stateToken = nanoid();
|
|
122
|
+
authUrl.searchParams.set("state", stateToken);
|
|
123
|
+
this._authUrl_ = authUrl.toString();
|
|
124
|
+
}
|
|
125
|
+
codeVerifierKey(clientId) {
|
|
126
|
+
return `${this.keyPrefix(clientId)}/code_verifier`;
|
|
127
|
+
}
|
|
128
|
+
async saveCodeVerifier(verifier) {
|
|
129
|
+
const key = this.codeVerifierKey(this.clientId);
|
|
130
|
+
if (await this.storage.get(key)) return;
|
|
131
|
+
await this.storage.put(key, verifier);
|
|
132
|
+
}
|
|
133
|
+
async codeVerifier() {
|
|
134
|
+
const codeVerifier = await this.storage.get(this.codeVerifierKey(this.clientId));
|
|
135
|
+
if (!codeVerifier) throw new Error("No code verifier found");
|
|
136
|
+
return codeVerifier;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region ../agents/dist/client-CFhjXCiO.js
|
|
47
142
|
function toDisposable(fn) {
|
|
48
143
|
return { dispose: fn };
|
|
49
144
|
}
|
|
@@ -91,73 +186,21 @@ function isTransportNotImplemented(error) {
|
|
|
91
186
|
const msg = toErrorMessage(error);
|
|
92
187
|
return msg.includes("404") || msg.includes("405") || msg.includes("Not Implemented") || msg.includes("not implemented");
|
|
93
188
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
delete workerOptions.mode;
|
|
110
|
-
return options.eventSourceInit?.fetch?.(fetchUrl, workerOptions) || fetch(fetchUrl, workerOptions);
|
|
111
|
-
};
|
|
112
|
-
super(url, {
|
|
113
|
-
...options,
|
|
114
|
-
eventSourceInit: {
|
|
115
|
-
...options.eventSourceInit,
|
|
116
|
-
fetch: fetchOverride
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
this.authProvider = options.authProvider;
|
|
120
|
-
}
|
|
121
|
-
async authHeaders() {
|
|
122
|
-
if (this.authProvider) {
|
|
123
|
-
const tokens = await this.authProvider.tokens();
|
|
124
|
-
if (tokens) return { Authorization: `Bearer ${tokens.access_token}` };
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
var StreamableHTTPEdgeClientTransport = class extends StreamableHTTPClientTransport {
|
|
129
|
-
/**
|
|
130
|
-
* Creates a new StreamableHTTPEdgeClientTransport, which overrides fetch to be compatible with the CF workers environment
|
|
131
|
-
*/
|
|
132
|
-
constructor(url, options) {
|
|
133
|
-
const fetchOverride = async (fetchUrl, fetchInit = {}) => {
|
|
134
|
-
const headers = await this.authHeaders();
|
|
135
|
-
const workerOptions = {
|
|
136
|
-
...fetchInit,
|
|
137
|
-
headers: {
|
|
138
|
-
...options.requestInit?.headers,
|
|
139
|
-
...fetchInit?.headers,
|
|
140
|
-
...headers
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
delete workerOptions.mode;
|
|
144
|
-
return options.requestInit?.fetch?.(fetchUrl, workerOptions) || fetch(fetchUrl, workerOptions);
|
|
145
|
-
};
|
|
146
|
-
super(url, {
|
|
147
|
-
...options,
|
|
148
|
-
requestInit: {
|
|
149
|
-
...options.requestInit,
|
|
150
|
-
fetch: fetchOverride
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
this.authProvider = options.authProvider;
|
|
154
|
-
}
|
|
155
|
-
async authHeaders() {
|
|
156
|
-
if (this.authProvider) {
|
|
157
|
-
const tokens = await this.authProvider.tokens();
|
|
158
|
-
if (tokens) return { Authorization: `Bearer ${tokens.access_token}` };
|
|
159
|
-
}
|
|
160
|
-
}
|
|
189
|
+
/**
|
|
190
|
+
* Connection state machine for MCP client connections.
|
|
191
|
+
*
|
|
192
|
+
* State transitions:
|
|
193
|
+
* - Non-OAuth: init() → CONNECTING → DISCOVERING → READY
|
|
194
|
+
* - OAuth: init() → AUTHENTICATING → (callback) → CONNECTING → DISCOVERING → READY
|
|
195
|
+
* - Any state can transition to FAILED on error
|
|
196
|
+
*/
|
|
197
|
+
const MCPConnectionState = {
|
|
198
|
+
AUTHENTICATING: "authenticating",
|
|
199
|
+
CONNECTING: "connecting",
|
|
200
|
+
CONNECTED: "connected",
|
|
201
|
+
DISCOVERING: "discovering",
|
|
202
|
+
READY: "ready",
|
|
203
|
+
FAILED: "failed"
|
|
161
204
|
};
|
|
162
205
|
var MCPClientConnection = class {
|
|
163
206
|
constructor(url, info, options = {
|
|
@@ -166,7 +209,7 @@ var MCPClientConnection = class {
|
|
|
166
209
|
}) {
|
|
167
210
|
this.url = url;
|
|
168
211
|
this.options = options;
|
|
169
|
-
this.connectionState =
|
|
212
|
+
this.connectionState = MCPConnectionState.CONNECTING;
|
|
170
213
|
this.tools = [];
|
|
171
214
|
this.prompts = [];
|
|
172
215
|
this.resources = [];
|
|
@@ -182,36 +225,49 @@ var MCPClientConnection = class {
|
|
|
182
225
|
});
|
|
183
226
|
}
|
|
184
227
|
/**
|
|
185
|
-
* Initialize a client connection
|
|
228
|
+
* Initialize a client connection, if authentication is required, the connection will be in the AUTHENTICATING state
|
|
229
|
+
* Sets connection state based on the result and emits observability events
|
|
186
230
|
*
|
|
187
|
-
* @returns
|
|
231
|
+
* @returns Error message if connection failed, undefined otherwise
|
|
188
232
|
*/
|
|
189
233
|
async init() {
|
|
190
234
|
const transportType = this.options.transport.type;
|
|
191
235
|
if (!transportType) throw new Error("Transport type must be specified");
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
this.
|
|
197
|
-
|
|
198
|
-
|
|
236
|
+
const res = await this.tryConnect(transportType);
|
|
237
|
+
this.connectionState = res.state;
|
|
238
|
+
if (res.state === MCPConnectionState.CONNECTED && res.transport) {
|
|
239
|
+
this.client.setRequestHandler(ElicitRequestSchema, async (request) => {
|
|
240
|
+
return await this.handleElicitationRequest(request);
|
|
241
|
+
});
|
|
242
|
+
this.lastConnectedTransport = res.transport;
|
|
199
243
|
this._onObservabilityEvent.fire({
|
|
200
244
|
type: "mcp:client:connect",
|
|
201
|
-
displayMessage: `
|
|
245
|
+
displayMessage: `Connected successfully using ${res.transport} transport for ${this.url.toString()}`,
|
|
246
|
+
payload: {
|
|
247
|
+
url: this.url.toString(),
|
|
248
|
+
transport: res.transport,
|
|
249
|
+
state: this.connectionState
|
|
250
|
+
},
|
|
251
|
+
timestamp: Date.now(),
|
|
252
|
+
id: nanoid()
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
} else if (res.state === MCPConnectionState.FAILED && res.error) {
|
|
256
|
+
const errorMessage = toErrorMessage(res.error);
|
|
257
|
+
this._onObservabilityEvent.fire({
|
|
258
|
+
type: "mcp:client:connect",
|
|
259
|
+
displayMessage: `Failed to connect to ${this.url.toString()}: ${errorMessage}`,
|
|
202
260
|
payload: {
|
|
203
261
|
url: this.url.toString(),
|
|
204
262
|
transport: transportType,
|
|
205
263
|
state: this.connectionState,
|
|
206
|
-
error:
|
|
264
|
+
error: errorMessage
|
|
207
265
|
},
|
|
208
266
|
timestamp: Date.now(),
|
|
209
267
|
id: nanoid()
|
|
210
268
|
});
|
|
211
|
-
|
|
212
|
-
return;
|
|
269
|
+
return errorMessage;
|
|
213
270
|
}
|
|
214
|
-
await this.discoverAndRegister();
|
|
215
271
|
}
|
|
216
272
|
/**
|
|
217
273
|
* Finish OAuth by probing transports based on configured type.
|
|
@@ -243,113 +299,189 @@ var MCPClientConnection = class {
|
|
|
243
299
|
* Complete OAuth authorization
|
|
244
300
|
*/
|
|
245
301
|
async completeAuthorization(code) {
|
|
246
|
-
if (this.connectionState !==
|
|
302
|
+
if (this.connectionState !== MCPConnectionState.AUTHENTICATING) throw new Error("Connection must be in authenticating state to complete authorization");
|
|
247
303
|
try {
|
|
248
304
|
await this.finishAuthProbe(code);
|
|
249
|
-
this.connectionState =
|
|
305
|
+
this.connectionState = MCPConnectionState.CONNECTING;
|
|
250
306
|
} catch (error) {
|
|
251
|
-
this.connectionState =
|
|
307
|
+
this.connectionState = MCPConnectionState.FAILED;
|
|
252
308
|
throw error;
|
|
253
309
|
}
|
|
254
310
|
}
|
|
255
311
|
/**
|
|
256
|
-
*
|
|
312
|
+
* Discover server capabilities and register tools, resources, prompts, and templates.
|
|
313
|
+
* This method does the work but does not manage connection state - that's handled by discover().
|
|
257
314
|
*/
|
|
258
|
-
async
|
|
259
|
-
|
|
315
|
+
async discoverAndRegister() {
|
|
316
|
+
this.serverCapabilities = this.client.getServerCapabilities();
|
|
317
|
+
if (!this.serverCapabilities) throw new Error("The MCP Server failed to return server capabilities");
|
|
318
|
+
const operations = [];
|
|
319
|
+
const operationNames = [];
|
|
320
|
+
operations.push(Promise.resolve(this.client.getInstructions()));
|
|
321
|
+
operationNames.push("instructions");
|
|
322
|
+
if (this.serverCapabilities.tools) {
|
|
323
|
+
operations.push(this.registerTools());
|
|
324
|
+
operationNames.push("tools");
|
|
325
|
+
}
|
|
326
|
+
if (this.serverCapabilities.resources) {
|
|
327
|
+
operations.push(this.registerResources());
|
|
328
|
+
operationNames.push("resources");
|
|
329
|
+
}
|
|
330
|
+
if (this.serverCapabilities.prompts) {
|
|
331
|
+
operations.push(this.registerPrompts());
|
|
332
|
+
operationNames.push("prompts");
|
|
333
|
+
}
|
|
334
|
+
if (this.serverCapabilities.resources) {
|
|
335
|
+
operations.push(this.registerResourceTemplates());
|
|
336
|
+
operationNames.push("resource templates");
|
|
337
|
+
}
|
|
260
338
|
try {
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
339
|
+
const results = await Promise.all(operations);
|
|
340
|
+
for (let i = 0; i < results.length; i++) {
|
|
341
|
+
const result = results[i];
|
|
342
|
+
switch (operationNames[i]) {
|
|
343
|
+
case "instructions":
|
|
344
|
+
this.instructions = result;
|
|
345
|
+
break;
|
|
346
|
+
case "tools":
|
|
347
|
+
this.tools = result;
|
|
348
|
+
break;
|
|
349
|
+
case "resources":
|
|
350
|
+
this.resources = result;
|
|
351
|
+
break;
|
|
352
|
+
case "prompts":
|
|
353
|
+
this.prompts = result;
|
|
354
|
+
break;
|
|
355
|
+
case "resource templates":
|
|
356
|
+
this.resourceTemplates = result;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
265
360
|
} catch (error) {
|
|
266
|
-
this.
|
|
361
|
+
this._onObservabilityEvent.fire({
|
|
362
|
+
type: "mcp:client:discover",
|
|
363
|
+
displayMessage: `Failed to discover capabilities for ${this.url.toString()}: ${toErrorMessage(error)}`,
|
|
364
|
+
payload: {
|
|
365
|
+
url: this.url.toString(),
|
|
366
|
+
error: toErrorMessage(error)
|
|
367
|
+
},
|
|
368
|
+
timestamp: Date.now(),
|
|
369
|
+
id: nanoid()
|
|
370
|
+
});
|
|
267
371
|
throw error;
|
|
268
372
|
}
|
|
269
373
|
}
|
|
270
374
|
/**
|
|
271
|
-
* Discover server capabilities
|
|
375
|
+
* Discover server capabilities with timeout and cancellation support.
|
|
376
|
+
* If called while a previous discovery is in-flight, the previous discovery will be aborted.
|
|
377
|
+
*
|
|
378
|
+
* @param options Optional configuration
|
|
379
|
+
* @param options.timeoutMs Timeout in milliseconds (default: 15000)
|
|
380
|
+
* @returns Result indicating success/failure with optional error message
|
|
272
381
|
*/
|
|
273
|
-
async
|
|
274
|
-
|
|
275
|
-
this.
|
|
276
|
-
if (!this.serverCapabilities) throw new Error("The MCP Server failed to return server capabilities");
|
|
277
|
-
const [instructionsResult, toolsResult, resourcesResult, promptsResult, resourceTemplatesResult] = await Promise.allSettled([
|
|
278
|
-
this.client.getInstructions(),
|
|
279
|
-
this.registerTools(),
|
|
280
|
-
this.registerResources(),
|
|
281
|
-
this.registerPrompts(),
|
|
282
|
-
this.registerResourceTemplates()
|
|
283
|
-
]);
|
|
284
|
-
const operations = [
|
|
285
|
-
{
|
|
286
|
-
name: "instructions",
|
|
287
|
-
result: instructionsResult
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
name: "tools",
|
|
291
|
-
result: toolsResult
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
name: "resources",
|
|
295
|
-
result: resourcesResult
|
|
296
|
-
},
|
|
297
|
-
{
|
|
298
|
-
name: "prompts",
|
|
299
|
-
result: promptsResult
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
name: "resource templates",
|
|
303
|
-
result: resourceTemplatesResult
|
|
304
|
-
}
|
|
305
|
-
];
|
|
306
|
-
for (const { name, result } of operations) if (result.status === "rejected") {
|
|
307
|
-
const url = this.url.toString();
|
|
382
|
+
async discover(options = {}) {
|
|
383
|
+
const { timeoutMs = 15e3 } = options;
|
|
384
|
+
if (this.connectionState !== MCPConnectionState.CONNECTED && this.connectionState !== MCPConnectionState.READY) {
|
|
308
385
|
this._onObservabilityEvent.fire({
|
|
309
386
|
type: "mcp:client:discover",
|
|
310
|
-
displayMessage: `
|
|
387
|
+
displayMessage: `Discovery skipped for ${this.url.toString()}, state is ${this.connectionState}`,
|
|
311
388
|
payload: {
|
|
312
|
-
url,
|
|
313
|
-
|
|
314
|
-
error: result.reason
|
|
389
|
+
url: this.url.toString(),
|
|
390
|
+
state: this.connectionState
|
|
315
391
|
},
|
|
316
392
|
timestamp: Date.now(),
|
|
317
393
|
id: nanoid()
|
|
318
394
|
});
|
|
395
|
+
return {
|
|
396
|
+
success: false,
|
|
397
|
+
error: `Discovery skipped - connection in ${this.connectionState} state`
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
if (this._discoveryAbortController) {
|
|
401
|
+
this._discoveryAbortController.abort();
|
|
402
|
+
this._discoveryAbortController = void 0;
|
|
403
|
+
}
|
|
404
|
+
const abortController = new AbortController();
|
|
405
|
+
this._discoveryAbortController = abortController;
|
|
406
|
+
this.connectionState = MCPConnectionState.DISCOVERING;
|
|
407
|
+
let timeoutId;
|
|
408
|
+
try {
|
|
409
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
410
|
+
timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error(`Discovery timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
411
|
+
});
|
|
412
|
+
if (abortController.signal.aborted) throw new Error("Discovery was cancelled");
|
|
413
|
+
const abortPromise = new Promise((_, reject) => {
|
|
414
|
+
abortController.signal.addEventListener("abort", () => {
|
|
415
|
+
reject(/* @__PURE__ */ new Error("Discovery was cancelled"));
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
await Promise.race([
|
|
419
|
+
this.discoverAndRegister(),
|
|
420
|
+
timeoutPromise,
|
|
421
|
+
abortPromise
|
|
422
|
+
]);
|
|
423
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
424
|
+
this.connectionState = MCPConnectionState.READY;
|
|
425
|
+
this._onObservabilityEvent.fire({
|
|
426
|
+
type: "mcp:client:discover",
|
|
427
|
+
displayMessage: `Discovery completed for ${this.url.toString()}`,
|
|
428
|
+
payload: { url: this.url.toString() },
|
|
429
|
+
timestamp: Date.now(),
|
|
430
|
+
id: nanoid()
|
|
431
|
+
});
|
|
432
|
+
return { success: true };
|
|
433
|
+
} catch (e) {
|
|
434
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
435
|
+
this.connectionState = MCPConnectionState.CONNECTED;
|
|
436
|
+
return {
|
|
437
|
+
success: false,
|
|
438
|
+
error: e instanceof Error ? e.message : String(e)
|
|
439
|
+
};
|
|
440
|
+
} finally {
|
|
441
|
+
this._discoveryAbortController = void 0;
|
|
319
442
|
}
|
|
320
|
-
this.instructions = instructionsResult.status === "fulfilled" ? instructionsResult.value : void 0;
|
|
321
|
-
this.tools = toolsResult.status === "fulfilled" ? toolsResult.value : [];
|
|
322
|
-
this.resources = resourcesResult.status === "fulfilled" ? resourcesResult.value : [];
|
|
323
|
-
this.prompts = promptsResult.status === "fulfilled" ? promptsResult.value : [];
|
|
324
|
-
this.resourceTemplates = resourceTemplatesResult.status === "fulfilled" ? resourceTemplatesResult.value : [];
|
|
325
|
-
this.connectionState = "ready";
|
|
326
443
|
}
|
|
327
444
|
/**
|
|
328
|
-
*
|
|
445
|
+
* Cancel any in-flight discovery operation.
|
|
446
|
+
* Called when closing the connection.
|
|
447
|
+
*/
|
|
448
|
+
cancelDiscovery() {
|
|
449
|
+
if (this._discoveryAbortController) {
|
|
450
|
+
this._discoveryAbortController.abort();
|
|
451
|
+
this._discoveryAbortController = void 0;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Notification handler registration for tools
|
|
456
|
+
* Should only be called if serverCapabilities.tools exists
|
|
329
457
|
*/
|
|
330
458
|
async registerTools() {
|
|
331
|
-
if (
|
|
332
|
-
if (this.serverCapabilities.tools.listChanged) this.client.setNotificationHandler(ToolListChangedNotificationSchema, async (_notification) => {
|
|
459
|
+
if (this.serverCapabilities?.tools?.listChanged) this.client.setNotificationHandler(ToolListChangedNotificationSchema, async (_notification) => {
|
|
333
460
|
this.tools = await this.fetchTools();
|
|
334
461
|
});
|
|
335
462
|
return this.fetchTools();
|
|
336
463
|
}
|
|
464
|
+
/**
|
|
465
|
+
* Notification handler registration for resources
|
|
466
|
+
* Should only be called if serverCapabilities.resources exists
|
|
467
|
+
*/
|
|
337
468
|
async registerResources() {
|
|
338
|
-
if (
|
|
339
|
-
if (this.serverCapabilities.resources.listChanged) this.client.setNotificationHandler(ResourceListChangedNotificationSchema, async (_notification) => {
|
|
469
|
+
if (this.serverCapabilities?.resources?.listChanged) this.client.setNotificationHandler(ResourceListChangedNotificationSchema, async (_notification) => {
|
|
340
470
|
this.resources = await this.fetchResources();
|
|
341
471
|
});
|
|
342
472
|
return this.fetchResources();
|
|
343
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Notification handler registration for prompts
|
|
476
|
+
* Should only be called if serverCapabilities.prompts exists
|
|
477
|
+
*/
|
|
344
478
|
async registerPrompts() {
|
|
345
|
-
if (
|
|
346
|
-
if (this.serverCapabilities.prompts.listChanged) this.client.setNotificationHandler(PromptListChangedNotificationSchema, async (_notification) => {
|
|
479
|
+
if (this.serverCapabilities?.prompts?.listChanged) this.client.setNotificationHandler(PromptListChangedNotificationSchema, async (_notification) => {
|
|
347
480
|
this.prompts = await this.fetchPrompts();
|
|
348
481
|
});
|
|
349
482
|
return this.fetchPrompts();
|
|
350
483
|
}
|
|
351
484
|
async registerResourceTemplates() {
|
|
352
|
-
if (!this.serverCapabilities || !this.serverCapabilities.resources) return [];
|
|
353
485
|
return this.fetchResourceTemplates();
|
|
354
486
|
}
|
|
355
487
|
async fetchTools() {
|
|
@@ -402,8 +534,8 @@ var MCPClientConnection = class {
|
|
|
402
534
|
*/
|
|
403
535
|
getTransport(transportType) {
|
|
404
536
|
switch (transportType) {
|
|
405
|
-
case "streamable-http": return new
|
|
406
|
-
case "sse": return new
|
|
537
|
+
case "streamable-http": return new StreamableHTTPClientTransport(this.url, this.options.transport);
|
|
538
|
+
case "sse": return new SSEClientTransport(this.url, this.options.transport);
|
|
407
539
|
default: throw new Error(`Unsupported transport type: ${transportType}`);
|
|
408
540
|
}
|
|
409
541
|
}
|
|
@@ -415,44 +547,24 @@ var MCPClientConnection = class {
|
|
|
415
547
|
const transport = this.getTransport(currentTransportType);
|
|
416
548
|
try {
|
|
417
549
|
await this.client.connect(transport);
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
displayMessage: `Connected successfully using ${currentTransportType} transport for ${url}`,
|
|
423
|
-
payload: {
|
|
424
|
-
url,
|
|
425
|
-
transport: currentTransportType,
|
|
426
|
-
state: this.connectionState
|
|
427
|
-
},
|
|
428
|
-
timestamp: Date.now(),
|
|
429
|
-
id: nanoid()
|
|
430
|
-
});
|
|
431
|
-
break;
|
|
550
|
+
return {
|
|
551
|
+
state: MCPConnectionState.CONNECTED,
|
|
552
|
+
transport: currentTransportType
|
|
553
|
+
};
|
|
432
554
|
} catch (e) {
|
|
433
555
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
434
|
-
if (isUnauthorized(error))
|
|
435
|
-
if (
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
payload: {
|
|
441
|
-
url,
|
|
442
|
-
transport: currentTransportType,
|
|
443
|
-
state: this.connectionState
|
|
444
|
-
},
|
|
445
|
-
timestamp: Date.now(),
|
|
446
|
-
id: nanoid()
|
|
447
|
-
});
|
|
448
|
-
continue;
|
|
449
|
-
}
|
|
450
|
-
throw e;
|
|
556
|
+
if (isUnauthorized(error)) return { state: MCPConnectionState.AUTHENTICATING };
|
|
557
|
+
if (isTransportNotImplemented(error) && hasFallback) continue;
|
|
558
|
+
return {
|
|
559
|
+
state: MCPConnectionState.FAILED,
|
|
560
|
+
error
|
|
561
|
+
};
|
|
451
562
|
}
|
|
452
563
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
564
|
+
return {
|
|
565
|
+
state: MCPConnectionState.FAILED,
|
|
566
|
+
error: /* @__PURE__ */ new Error("No transports available")
|
|
567
|
+
};
|
|
456
568
|
}
|
|
457
569
|
_capabilityErrorHandler(empty, method) {
|
|
458
570
|
return (e) => {
|
|
@@ -475,6 +587,7 @@ var MCPClientConnection = class {
|
|
|
475
587
|
};
|
|
476
588
|
}
|
|
477
589
|
};
|
|
590
|
+
const defaultClientOptions = { jsonSchemaValidator: new CfWorkerJsonSchemaValidator() };
|
|
478
591
|
/**
|
|
479
592
|
* Utility class that aggregates multiple MCP clients into one
|
|
480
593
|
*/
|
|
@@ -482,26 +595,121 @@ var MCPClientManager = class {
|
|
|
482
595
|
/**
|
|
483
596
|
* @param _name Name of the MCP client
|
|
484
597
|
* @param _version Version of the MCP Client
|
|
485
|
-
* @param
|
|
598
|
+
* @param options Storage adapter for persisting MCP server state
|
|
486
599
|
*/
|
|
487
|
-
constructor(_name, _version) {
|
|
600
|
+
constructor(_name, _version, options) {
|
|
488
601
|
this._name = _name;
|
|
489
602
|
this._version = _version;
|
|
490
603
|
this.mcpConnections = {};
|
|
491
|
-
this._callbackUrls = [];
|
|
492
604
|
this._didWarnAboutUnstableGetAITools = false;
|
|
493
605
|
this._connectionDisposables = /* @__PURE__ */ new Map();
|
|
606
|
+
this._isRestored = false;
|
|
494
607
|
this._onObservabilityEvent = new Emitter();
|
|
495
608
|
this.onObservabilityEvent = this._onObservabilityEvent.event;
|
|
496
|
-
this.
|
|
497
|
-
this.
|
|
609
|
+
this._onServerStateChanged = new Emitter();
|
|
610
|
+
this.onServerStateChanged = this._onServerStateChanged.event;
|
|
611
|
+
if (!options.storage) throw new Error("MCPClientManager requires a valid DurableObjectStorage instance");
|
|
612
|
+
this._storage = options.storage;
|
|
613
|
+
}
|
|
614
|
+
sql(query, ...bindings) {
|
|
615
|
+
return [...this._storage.sql.exec(query, ...bindings)];
|
|
616
|
+
}
|
|
617
|
+
saveServerToStorage(server) {
|
|
618
|
+
this.sql(`INSERT OR REPLACE INTO cf_agents_mcp_servers (
|
|
619
|
+
id, name, server_url, client_id, auth_url, callback_url, server_options
|
|
620
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`, server.id, server.name, server.server_url, server.client_id ?? null, server.auth_url ?? null, server.callback_url, server.server_options ?? null);
|
|
621
|
+
}
|
|
622
|
+
removeServerFromStorage(serverId) {
|
|
623
|
+
this.sql("DELETE FROM cf_agents_mcp_servers WHERE id = ?", serverId);
|
|
624
|
+
}
|
|
625
|
+
getServersFromStorage() {
|
|
626
|
+
return this.sql("SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers");
|
|
627
|
+
}
|
|
628
|
+
clearServerAuthUrl(serverId) {
|
|
629
|
+
this.sql("UPDATE cf_agents_mcp_servers SET auth_url = NULL WHERE id = ?", serverId);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Create an auth provider for a server
|
|
633
|
+
* @internal
|
|
634
|
+
*/
|
|
635
|
+
createAuthProvider(serverId, callbackUrl, clientName, clientId) {
|
|
636
|
+
if (!this._storage) throw new Error("Cannot create auth provider: storage is not initialized");
|
|
637
|
+
const authProvider = new DurableObjectOAuthClientProvider(this._storage, clientName, callbackUrl);
|
|
638
|
+
authProvider.serverId = serverId;
|
|
639
|
+
if (clientId) authProvider.clientId = clientId;
|
|
640
|
+
return authProvider;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Restore MCP server connections from storage
|
|
644
|
+
* This method is called on Agent initialization to restore previously connected servers
|
|
645
|
+
*
|
|
646
|
+
* @param clientName Name to use for OAuth client (typically the agent instance name)
|
|
647
|
+
*/
|
|
648
|
+
async restoreConnectionsFromStorage(clientName) {
|
|
649
|
+
if (this._isRestored) return;
|
|
650
|
+
const servers = this.getServersFromStorage();
|
|
651
|
+
if (!servers || servers.length === 0) {
|
|
652
|
+
this._isRestored = true;
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
for (const server of servers) {
|
|
656
|
+
const existingConn = this.mcpConnections[server.id];
|
|
657
|
+
if (existingConn) {
|
|
658
|
+
if (existingConn.connectionState === MCPConnectionState.READY) {
|
|
659
|
+
console.warn(`[MCPClientManager] Server ${server.id} already has a ready connection. Skipping recreation.`);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (existingConn.connectionState === MCPConnectionState.AUTHENTICATING || existingConn.connectionState === MCPConnectionState.CONNECTING || existingConn.connectionState === MCPConnectionState.DISCOVERING) continue;
|
|
663
|
+
if (existingConn.connectionState === MCPConnectionState.FAILED) {
|
|
664
|
+
try {
|
|
665
|
+
await existingConn.client.close();
|
|
666
|
+
} catch (error) {
|
|
667
|
+
console.warn(`[MCPClientManager] Error closing failed connection ${server.id}:`, error);
|
|
668
|
+
}
|
|
669
|
+
delete this.mcpConnections[server.id];
|
|
670
|
+
this._connectionDisposables.get(server.id)?.dispose();
|
|
671
|
+
this._connectionDisposables.delete(server.id);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const parsedOptions = server.server_options ? JSON.parse(server.server_options) : null;
|
|
675
|
+
const authProvider = this.createAuthProvider(server.id, server.callback_url, clientName, server.client_id ?? void 0);
|
|
676
|
+
const conn = this.createConnection(server.id, server.server_url, {
|
|
677
|
+
client: parsedOptions?.client ?? {},
|
|
678
|
+
transport: {
|
|
679
|
+
...parsedOptions?.transport ?? {},
|
|
680
|
+
type: parsedOptions?.transport?.type ?? "auto",
|
|
681
|
+
authProvider
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
if (server.auth_url) {
|
|
685
|
+
conn.connectionState = MCPConnectionState.AUTHENTICATING;
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
this._restoreServer(server.id);
|
|
689
|
+
}
|
|
690
|
+
this._isRestored = true;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Internal method to restore a single server connection and discovery
|
|
694
|
+
*/
|
|
695
|
+
async _restoreServer(serverId) {
|
|
696
|
+
if ((await this.connectToServer(serverId).catch((error) => {
|
|
697
|
+
console.error(`Error connecting to ${serverId}:`, error);
|
|
698
|
+
return null;
|
|
699
|
+
}))?.state === MCPConnectionState.CONNECTED) {
|
|
700
|
+
const discoverResult = await this.discoverIfConnected(serverId);
|
|
701
|
+
if (discoverResult && !discoverResult.success) console.error(`Error discovering ${serverId}:`, discoverResult.error);
|
|
702
|
+
}
|
|
498
703
|
}
|
|
499
704
|
/**
|
|
500
705
|
* Connect to and register an MCP server
|
|
501
706
|
*
|
|
502
|
-
* @
|
|
503
|
-
*
|
|
504
|
-
*
|
|
707
|
+
* @deprecated This method is maintained for backward compatibility.
|
|
708
|
+
* For new code, use registerServer() and connectToServer() separately.
|
|
709
|
+
*
|
|
710
|
+
* @param url Server URL
|
|
711
|
+
* @param options Connection options
|
|
712
|
+
* @returns Object with server ID, auth URL (if OAuth), and client ID (if OAuth)
|
|
505
713
|
*/
|
|
506
714
|
async connect(url, options = {}) {
|
|
507
715
|
/**
|
|
@@ -511,10 +719,7 @@ var MCPClientManager = class {
|
|
|
511
719
|
* .connect() is called on at least one server.
|
|
512
720
|
* So it's safe to delay loading it until .connect() is called.
|
|
513
721
|
*/
|
|
514
|
-
|
|
515
|
-
const { jsonSchema } = await import("ai");
|
|
516
|
-
this.jsonSchema = jsonSchema;
|
|
517
|
-
}
|
|
722
|
+
await this.ensureJsonSchema();
|
|
518
723
|
const id = options.reconnect?.id ?? nanoid(8);
|
|
519
724
|
if (options.transport?.authProvider) {
|
|
520
725
|
options.transport.authProvider.serverId = id;
|
|
@@ -543,7 +748,7 @@ var MCPClientManager = class {
|
|
|
543
748
|
await this.mcpConnections[id].init();
|
|
544
749
|
if (options.reconnect?.oauthCode) try {
|
|
545
750
|
await this.mcpConnections[id].completeAuthorization(options.reconnect.oauthCode);
|
|
546
|
-
await this.mcpConnections[id].
|
|
751
|
+
await this.mcpConnections[id].init();
|
|
547
752
|
} catch (error) {
|
|
548
753
|
this._onObservabilityEvent.fire({
|
|
549
754
|
type: "mcp:client:connect",
|
|
@@ -560,33 +765,144 @@ var MCPClientManager = class {
|
|
|
560
765
|
throw error;
|
|
561
766
|
}
|
|
562
767
|
const authUrl = options.transport?.authProvider?.authUrl;
|
|
563
|
-
if (this.mcpConnections[id].connectionState ===
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
768
|
+
if (this.mcpConnections[id].connectionState === MCPConnectionState.AUTHENTICATING && authUrl && options.transport?.authProvider?.redirectUrl) return {
|
|
769
|
+
authUrl,
|
|
770
|
+
clientId: options.transport?.authProvider?.clientId,
|
|
771
|
+
id
|
|
772
|
+
};
|
|
773
|
+
const discoverResult = await this.discoverIfConnected(id);
|
|
774
|
+
if (discoverResult && !discoverResult.success) throw new Error(`Failed to discover server capabilities: ${discoverResult.error}`);
|
|
775
|
+
return { id };
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Create an in-memory connection object and set up observability
|
|
779
|
+
* Does NOT save to storage - use registerServer() for that
|
|
780
|
+
* @returns The connection object (existing or newly created)
|
|
781
|
+
*/
|
|
782
|
+
createConnection(id, url, options) {
|
|
783
|
+
if (this.mcpConnections[id]) return this.mcpConnections[id];
|
|
784
|
+
const normalizedTransport = {
|
|
785
|
+
...options.transport,
|
|
786
|
+
type: options.transport?.type ?? "auto"
|
|
787
|
+
};
|
|
788
|
+
this.mcpConnections[id] = new MCPClientConnection(new URL(url), {
|
|
789
|
+
name: this._name,
|
|
790
|
+
version: this._version
|
|
791
|
+
}, {
|
|
792
|
+
client: {
|
|
793
|
+
...defaultClientOptions,
|
|
794
|
+
...options.client
|
|
795
|
+
},
|
|
796
|
+
transport: normalizedTransport
|
|
797
|
+
});
|
|
798
|
+
const store = new DisposableStore();
|
|
799
|
+
const existing = this._connectionDisposables.get(id);
|
|
800
|
+
if (existing) existing.dispose();
|
|
801
|
+
this._connectionDisposables.set(id, store);
|
|
802
|
+
store.add(this.mcpConnections[id].onObservabilityEvent((event) => {
|
|
803
|
+
this._onObservabilityEvent.fire(event);
|
|
804
|
+
}));
|
|
805
|
+
return this.mcpConnections[id];
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Register an MCP server connection without connecting
|
|
809
|
+
* Creates the connection object, sets up observability, and saves to storage
|
|
810
|
+
*
|
|
811
|
+
* @param id Server ID
|
|
812
|
+
* @param options Registration options including URL, name, callback URL, and connection config
|
|
813
|
+
* @returns Server ID
|
|
814
|
+
*/
|
|
815
|
+
async registerServer(id, options) {
|
|
816
|
+
this.createConnection(id, options.url, {
|
|
817
|
+
client: options.client,
|
|
818
|
+
transport: {
|
|
819
|
+
...options.transport,
|
|
820
|
+
type: options.transport?.type ?? "auto"
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
const { authProvider: _, ...transportWithoutAuth } = options.transport ?? {};
|
|
824
|
+
this.saveServerToStorage({
|
|
825
|
+
id,
|
|
826
|
+
name: options.name,
|
|
827
|
+
server_url: options.url,
|
|
828
|
+
callback_url: options.callbackUrl,
|
|
829
|
+
client_id: options.clientId ?? null,
|
|
830
|
+
auth_url: options.authUrl ?? null,
|
|
831
|
+
server_options: JSON.stringify({
|
|
832
|
+
client: options.client,
|
|
833
|
+
transport: transportWithoutAuth
|
|
834
|
+
})
|
|
835
|
+
});
|
|
836
|
+
this._onServerStateChanged.fire();
|
|
837
|
+
return id;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Connect to an already registered MCP server and initialize the connection.
|
|
841
|
+
*
|
|
842
|
+
* For OAuth servers, returns `{ state: "authenticating", authUrl, clientId? }`.
|
|
843
|
+
* The user must complete the OAuth flow via the authUrl, which triggers a
|
|
844
|
+
* callback handled by `handleCallbackRequest()`.
|
|
845
|
+
*
|
|
846
|
+
* For non-OAuth servers, establishes the transport connection and returns
|
|
847
|
+
* `{ state: "connected" }`. Call `discoverIfConnected()` afterwards to
|
|
848
|
+
* discover capabilities and transition to "ready" state.
|
|
849
|
+
*
|
|
850
|
+
* @param id Server ID (must be registered first via registerServer())
|
|
851
|
+
* @returns Connection result with current state and OAuth info (if applicable)
|
|
852
|
+
*/
|
|
853
|
+
async connectToServer(id) {
|
|
854
|
+
const conn = this.mcpConnections[id];
|
|
855
|
+
if (!conn) throw new Error(`Server ${id} is not registered. Call registerServer() first.`);
|
|
856
|
+
const error = await conn.init();
|
|
857
|
+
this._onServerStateChanged.fire();
|
|
858
|
+
switch (conn.connectionState) {
|
|
859
|
+
case MCPConnectionState.FAILED: return {
|
|
860
|
+
state: conn.connectionState,
|
|
861
|
+
error: error ?? "Unknown connection error"
|
|
862
|
+
};
|
|
863
|
+
case MCPConnectionState.AUTHENTICATING: {
|
|
864
|
+
const authUrl = conn.options.transport.authProvider?.authUrl;
|
|
865
|
+
const redirectUrl = conn.options.transport.authProvider?.redirectUrl;
|
|
866
|
+
if (!authUrl || !redirectUrl) return {
|
|
867
|
+
state: MCPConnectionState.FAILED,
|
|
868
|
+
error: `OAuth configuration incomplete: missing ${!authUrl ? "authUrl" : "redirectUrl"}`
|
|
869
|
+
};
|
|
870
|
+
const clientId = conn.options.transport.authProvider?.clientId;
|
|
871
|
+
const serverRow = this.getServersFromStorage().find((s) => s.id === id);
|
|
872
|
+
if (serverRow) this.saveServerToStorage({
|
|
873
|
+
...serverRow,
|
|
874
|
+
auth_url: authUrl,
|
|
875
|
+
client_id: clientId ?? null
|
|
876
|
+
});
|
|
877
|
+
return {
|
|
878
|
+
state: conn.connectionState,
|
|
879
|
+
authUrl,
|
|
880
|
+
clientId
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
case MCPConnectionState.CONNECTED: return { state: conn.connectionState };
|
|
884
|
+
default: return {
|
|
885
|
+
state: MCPConnectionState.FAILED,
|
|
886
|
+
error: `Unexpected connection state after init: ${conn.connectionState}`
|
|
569
887
|
};
|
|
570
888
|
}
|
|
571
|
-
return { id };
|
|
572
889
|
}
|
|
573
890
|
isCallbackRequest(req) {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
891
|
+
if (req.method !== "GET") return false;
|
|
892
|
+
if (!req.url.includes("/callback")) return false;
|
|
893
|
+
return this.getServersFromStorage().some((server) => server.callback_url && req.url.startsWith(server.callback_url));
|
|
577
894
|
}
|
|
578
895
|
async handleCallbackRequest(req) {
|
|
579
896
|
const url = new URL(req.url);
|
|
580
|
-
const
|
|
581
|
-
return req.url.startsWith(
|
|
897
|
+
const matchingServer = this.getServersFromStorage().find((server) => {
|
|
898
|
+
return server.callback_url && req.url.startsWith(server.callback_url);
|
|
582
899
|
});
|
|
583
|
-
if (!
|
|
900
|
+
if (!matchingServer) throw new Error(`No callback URI match found for the request url: ${req.url}. Was the request matched with \`isCallbackRequest()\`?`);
|
|
901
|
+
const serverId = matchingServer.id;
|
|
584
902
|
const code = url.searchParams.get("code");
|
|
585
903
|
const state = url.searchParams.get("state");
|
|
586
904
|
const error = url.searchParams.get("error");
|
|
587
905
|
const errorDescription = url.searchParams.get("error_description");
|
|
588
|
-
const urlParams = urlMatch.split("/");
|
|
589
|
-
const serverId = urlParams[urlParams.length - 1];
|
|
590
906
|
if (error) return {
|
|
591
907
|
serverId,
|
|
592
908
|
authSuccess: false,
|
|
@@ -595,7 +911,14 @@ var MCPClientManager = class {
|
|
|
595
911
|
if (!code) throw new Error("Unauthorized: no code provided");
|
|
596
912
|
if (!state) throw new Error("Unauthorized: no state provided");
|
|
597
913
|
if (this.mcpConnections[serverId] === void 0) throw new Error(`Could not find serverId: ${serverId}`);
|
|
598
|
-
if (this.mcpConnections[serverId].connectionState
|
|
914
|
+
if (this.mcpConnections[serverId].connectionState === MCPConnectionState.READY || this.mcpConnections[serverId].connectionState === MCPConnectionState.CONNECTED) {
|
|
915
|
+
this.clearServerAuthUrl(serverId);
|
|
916
|
+
return {
|
|
917
|
+
serverId,
|
|
918
|
+
authSuccess: true
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
if (this.mcpConnections[serverId].connectionState !== MCPConnectionState.AUTHENTICATING) throw new Error(`Failed to authenticate: the client is in "${this.mcpConnections[serverId].connectionState}" state, expected "authenticating"`);
|
|
599
922
|
const conn = this.mcpConnections[serverId];
|
|
600
923
|
if (!conn.options.transport.authProvider) throw new Error("Trying to finalize authentication for a server connection without an authProvider");
|
|
601
924
|
const clientId = conn.options.transport.authProvider.clientId || state;
|
|
@@ -603,21 +926,57 @@ var MCPClientManager = class {
|
|
|
603
926
|
conn.options.transport.authProvider.serverId = serverId;
|
|
604
927
|
try {
|
|
605
928
|
await conn.completeAuthorization(code);
|
|
929
|
+
this.clearServerAuthUrl(serverId);
|
|
930
|
+
this._onServerStateChanged.fire();
|
|
606
931
|
return {
|
|
607
932
|
serverId,
|
|
608
933
|
authSuccess: true
|
|
609
934
|
};
|
|
610
935
|
} catch (error$1) {
|
|
936
|
+
const errorMessage = error$1 instanceof Error ? error$1.message : String(error$1);
|
|
937
|
+
this._onServerStateChanged.fire();
|
|
611
938
|
return {
|
|
612
939
|
serverId,
|
|
613
940
|
authSuccess: false,
|
|
614
|
-
authError:
|
|
941
|
+
authError: errorMessage
|
|
615
942
|
};
|
|
616
943
|
}
|
|
617
944
|
}
|
|
618
945
|
/**
|
|
946
|
+
* Discover server capabilities if connection is in CONNECTED or READY state.
|
|
947
|
+
* Transitions to DISCOVERING then READY (or CONNECTED on error).
|
|
948
|
+
* Can be called to refresh server capabilities (e.g., from a UI refresh button).
|
|
949
|
+
*
|
|
950
|
+
* If called while a previous discovery is in-flight for the same server,
|
|
951
|
+
* the previous discovery will be aborted.
|
|
952
|
+
*
|
|
953
|
+
* @param serverId The server ID to discover
|
|
954
|
+
* @param options Optional configuration
|
|
955
|
+
* @param options.timeoutMs Timeout in milliseconds (default: 30000)
|
|
956
|
+
* @returns Result with current state and optional error, or undefined if connection not found
|
|
957
|
+
*/
|
|
958
|
+
async discoverIfConnected(serverId, options = {}) {
|
|
959
|
+
const conn = this.mcpConnections[serverId];
|
|
960
|
+
if (!conn) {
|
|
961
|
+
this._onObservabilityEvent.fire({
|
|
962
|
+
type: "mcp:client:discover",
|
|
963
|
+
displayMessage: `Connection not found for ${serverId}`,
|
|
964
|
+
payload: {},
|
|
965
|
+
timestamp: Date.now(),
|
|
966
|
+
id: nanoid()
|
|
967
|
+
});
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const result = await conn.discover(options);
|
|
971
|
+
this._onServerStateChanged.fire();
|
|
972
|
+
return {
|
|
973
|
+
...result,
|
|
974
|
+
state: conn.connectionState
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
619
978
|
* Establish connection in the background after OAuth completion
|
|
620
|
-
* This method
|
|
979
|
+
* This method connects to the server and discovers its capabilities
|
|
621
980
|
* @param serverId The server ID to establish connection for
|
|
622
981
|
*/
|
|
623
982
|
async establishConnection(serverId) {
|
|
@@ -632,38 +991,34 @@ var MCPClientManager = class {
|
|
|
632
991
|
});
|
|
633
992
|
return;
|
|
634
993
|
}
|
|
635
|
-
|
|
636
|
-
await conn.establishConnection();
|
|
637
|
-
this._onConnected.fire(serverId);
|
|
638
|
-
} catch (error) {
|
|
639
|
-
const url = conn.url.toString();
|
|
994
|
+
if (conn.connectionState === MCPConnectionState.DISCOVERING || conn.connectionState === MCPConnectionState.READY) {
|
|
640
995
|
this._onObservabilityEvent.fire({
|
|
641
996
|
type: "mcp:client:connect",
|
|
642
|
-
displayMessage: `
|
|
997
|
+
displayMessage: `establishConnection skipped for ${serverId}, already in ${conn.connectionState} state`,
|
|
643
998
|
payload: {
|
|
644
|
-
url,
|
|
645
|
-
transport: conn.options.transport.type
|
|
646
|
-
state: conn.connectionState
|
|
647
|
-
error: toErrorMessage(error)
|
|
999
|
+
url: conn.url.toString(),
|
|
1000
|
+
transport: conn.options.transport.type || "unknown",
|
|
1001
|
+
state: conn.connectionState
|
|
648
1002
|
},
|
|
649
1003
|
timestamp: Date.now(),
|
|
650
1004
|
id: nanoid()
|
|
651
1005
|
});
|
|
1006
|
+
return;
|
|
652
1007
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1008
|
+
const connectResult = await this.connectToServer(serverId);
|
|
1009
|
+
this._onServerStateChanged.fire();
|
|
1010
|
+
if (connectResult.state === MCPConnectionState.CONNECTED) await this.discoverIfConnected(serverId);
|
|
1011
|
+
this._onObservabilityEvent.fire({
|
|
1012
|
+
type: "mcp:client:connect",
|
|
1013
|
+
displayMessage: `establishConnection completed for ${serverId}, final state: ${conn.connectionState}`,
|
|
1014
|
+
payload: {
|
|
1015
|
+
url: conn.url.toString(),
|
|
1016
|
+
transport: conn.options.transport.type || "unknown",
|
|
1017
|
+
state: conn.connectionState
|
|
1018
|
+
},
|
|
1019
|
+
timestamp: Date.now(),
|
|
1020
|
+
id: nanoid()
|
|
1021
|
+
});
|
|
667
1022
|
}
|
|
668
1023
|
/**
|
|
669
1024
|
* Configure OAuth callback handling
|
|
@@ -686,9 +1041,28 @@ var MCPClientManager = class {
|
|
|
686
1041
|
return getNamespacedData(this.mcpConnections, "tools");
|
|
687
1042
|
}
|
|
688
1043
|
/**
|
|
1044
|
+
* Lazy-loads the jsonSchema function from the AI SDK.
|
|
1045
|
+
*
|
|
1046
|
+
* This defers importing the "ai" package until it's actually needed, which helps reduce
|
|
1047
|
+
* initial bundle size and startup time. The jsonSchema function is required for converting
|
|
1048
|
+
* MCP tools into AI SDK tool definitions via getAITools().
|
|
1049
|
+
*
|
|
1050
|
+
* @internal This method is for internal use only. It's automatically called before operations
|
|
1051
|
+
* that need jsonSchema (like getAITools() or OAuth flows). External consumers should not need
|
|
1052
|
+
* to call this directly.
|
|
1053
|
+
*/
|
|
1054
|
+
async ensureJsonSchema() {
|
|
1055
|
+
if (!this.jsonSchema) {
|
|
1056
|
+
const { jsonSchema } = await import("ai");
|
|
1057
|
+
this.jsonSchema = jsonSchema;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
689
1061
|
* @returns a set of tools that you can use with the AI SDK
|
|
690
1062
|
*/
|
|
691
1063
|
getAITools() {
|
|
1064
|
+
if (!this.jsonSchema) throw new Error("jsonSchema not initialized.");
|
|
1065
|
+
for (const [id, conn] of Object.entries(this.mcpConnections)) if (conn.connectionState !== MCPConnectionState.READY && conn.connectionState !== MCPConnectionState.AUTHENTICATING) console.warn(`[getAITools] WARNING: Reading tools from connection ${id} in state "${conn.connectionState}". Tools may not be loaded yet.`);
|
|
692
1066
|
return Object.fromEntries(getNamespacedData(this.mcpConnections, "tools").map((tool) => {
|
|
693
1067
|
return [`tool_${tool.serverId.replace(/-/g, "")}_${tool.name}`, {
|
|
694
1068
|
description: tool.description,
|
|
@@ -718,10 +1092,18 @@ var MCPClientManager = class {
|
|
|
718
1092
|
return this.getAITools();
|
|
719
1093
|
}
|
|
720
1094
|
/**
|
|
721
|
-
* Closes all connections to MCP servers
|
|
1095
|
+
* Closes all active in-memory connections to MCP servers.
|
|
1096
|
+
*
|
|
1097
|
+
* Note: This only closes the transport connections - it does NOT remove
|
|
1098
|
+
* servers from storage. Servers will still be listed and their callback
|
|
1099
|
+
* URLs will still match incoming OAuth requests.
|
|
1100
|
+
*
|
|
1101
|
+
* Use removeServer() instead if you want to fully clean up a server
|
|
1102
|
+
* (closes connection AND removes from storage).
|
|
722
1103
|
*/
|
|
723
1104
|
async closeAllConnections() {
|
|
724
1105
|
const ids = Object.keys(this.mcpConnections);
|
|
1106
|
+
for (const id of ids) this.mcpConnections[id].cancelDiscovery();
|
|
725
1107
|
await Promise.all(ids.map(async (id) => {
|
|
726
1108
|
await this.mcpConnections[id].client.close();
|
|
727
1109
|
}));
|
|
@@ -738,6 +1120,7 @@ var MCPClientManager = class {
|
|
|
738
1120
|
*/
|
|
739
1121
|
async closeConnection(id) {
|
|
740
1122
|
if (!this.mcpConnections[id]) throw new Error(`Connection with id "${id}" does not exist.`);
|
|
1123
|
+
this.mcpConnections[id].cancelDiscovery();
|
|
741
1124
|
await this.mcpConnections[id].client.close();
|
|
742
1125
|
delete this.mcpConnections[id];
|
|
743
1126
|
const store = this._connectionDisposables.get(id);
|
|
@@ -745,13 +1128,29 @@ var MCPClientManager = class {
|
|
|
745
1128
|
this._connectionDisposables.delete(id);
|
|
746
1129
|
}
|
|
747
1130
|
/**
|
|
1131
|
+
* Remove an MCP server - closes connection if active and removes from storage.
|
|
1132
|
+
*/
|
|
1133
|
+
async removeServer(serverId) {
|
|
1134
|
+
if (this.mcpConnections[serverId]) try {
|
|
1135
|
+
await this.closeConnection(serverId);
|
|
1136
|
+
} catch (_e) {}
|
|
1137
|
+
this.removeServerFromStorage(serverId);
|
|
1138
|
+
this._onServerStateChanged.fire();
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* List all MCP servers from storage
|
|
1142
|
+
*/
|
|
1143
|
+
listServers() {
|
|
1144
|
+
return this.getServersFromStorage();
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
748
1147
|
* Dispose the manager and all resources.
|
|
749
1148
|
*/
|
|
750
1149
|
async dispose() {
|
|
751
1150
|
try {
|
|
752
1151
|
await this.closeAllConnections();
|
|
753
1152
|
} finally {
|
|
754
|
-
this.
|
|
1153
|
+
this._onServerStateChanged.dispose();
|
|
755
1154
|
this._onObservabilityEvent.dispose();
|
|
756
1155
|
}
|
|
757
1156
|
}
|
|
@@ -813,96 +1212,7 @@ function getNamespacedData(mcpClients, type) {
|
|
|
813
1212
|
}
|
|
814
1213
|
|
|
815
1214
|
//#endregion
|
|
816
|
-
//#region ../agents/dist/
|
|
817
|
-
var DurableObjectOAuthClientProvider = class {
|
|
818
|
-
constructor(storage, clientName, baseRedirectUrl) {
|
|
819
|
-
this.storage = storage;
|
|
820
|
-
this.clientName = clientName;
|
|
821
|
-
this.baseRedirectUrl = baseRedirectUrl;
|
|
822
|
-
}
|
|
823
|
-
get clientMetadata() {
|
|
824
|
-
return {
|
|
825
|
-
client_name: this.clientName,
|
|
826
|
-
client_uri: this.clientUri,
|
|
827
|
-
grant_types: ["authorization_code", "refresh_token"],
|
|
828
|
-
redirect_uris: [this.redirectUrl],
|
|
829
|
-
response_types: ["code"],
|
|
830
|
-
token_endpoint_auth_method: "none"
|
|
831
|
-
};
|
|
832
|
-
}
|
|
833
|
-
get clientUri() {
|
|
834
|
-
return new URL(this.redirectUrl).origin;
|
|
835
|
-
}
|
|
836
|
-
get redirectUrl() {
|
|
837
|
-
return `${this.baseRedirectUrl}/${this.serverId}`;
|
|
838
|
-
}
|
|
839
|
-
get clientId() {
|
|
840
|
-
if (!this._clientId_) throw new Error("Trying to access clientId before it was set");
|
|
841
|
-
return this._clientId_;
|
|
842
|
-
}
|
|
843
|
-
set clientId(clientId_) {
|
|
844
|
-
this._clientId_ = clientId_;
|
|
845
|
-
}
|
|
846
|
-
get serverId() {
|
|
847
|
-
if (!this._serverId_) throw new Error("Trying to access serverId before it was set");
|
|
848
|
-
return this._serverId_;
|
|
849
|
-
}
|
|
850
|
-
set serverId(serverId_) {
|
|
851
|
-
this._serverId_ = serverId_;
|
|
852
|
-
}
|
|
853
|
-
keyPrefix(clientId) {
|
|
854
|
-
return `/${this.clientName}/${this.serverId}/${clientId}`;
|
|
855
|
-
}
|
|
856
|
-
clientInfoKey(clientId) {
|
|
857
|
-
return `${this.keyPrefix(clientId)}/client_info/`;
|
|
858
|
-
}
|
|
859
|
-
async clientInformation() {
|
|
860
|
-
if (!this._clientId_) return;
|
|
861
|
-
return await this.storage.get(this.clientInfoKey(this.clientId)) ?? void 0;
|
|
862
|
-
}
|
|
863
|
-
async saveClientInformation(clientInformation) {
|
|
864
|
-
await this.storage.put(this.clientInfoKey(clientInformation.client_id), clientInformation);
|
|
865
|
-
this.clientId = clientInformation.client_id;
|
|
866
|
-
}
|
|
867
|
-
tokenKey(clientId) {
|
|
868
|
-
return `${this.keyPrefix(clientId)}/token`;
|
|
869
|
-
}
|
|
870
|
-
async tokens() {
|
|
871
|
-
if (!this._clientId_) return;
|
|
872
|
-
return await this.storage.get(this.tokenKey(this.clientId)) ?? void 0;
|
|
873
|
-
}
|
|
874
|
-
async saveTokens(tokens) {
|
|
875
|
-
await this.storage.put(this.tokenKey(this.clientId), tokens);
|
|
876
|
-
}
|
|
877
|
-
get authUrl() {
|
|
878
|
-
return this._authUrl_;
|
|
879
|
-
}
|
|
880
|
-
/**
|
|
881
|
-
* Because this operates on the server side (but we need browser auth), we send this url back to the user
|
|
882
|
-
* and require user interact to initiate the redirect flow
|
|
883
|
-
*/
|
|
884
|
-
async redirectToAuthorization(authUrl) {
|
|
885
|
-
const stateToken = nanoid();
|
|
886
|
-
authUrl.searchParams.set("state", stateToken);
|
|
887
|
-
this._authUrl_ = authUrl.toString();
|
|
888
|
-
}
|
|
889
|
-
codeVerifierKey(clientId) {
|
|
890
|
-
return `${this.keyPrefix(clientId)}/code_verifier`;
|
|
891
|
-
}
|
|
892
|
-
async saveCodeVerifier(verifier) {
|
|
893
|
-
const key = this.codeVerifierKey(this.clientId);
|
|
894
|
-
if (await this.storage.get(key)) return;
|
|
895
|
-
await this.storage.put(key, verifier);
|
|
896
|
-
}
|
|
897
|
-
async codeVerifier() {
|
|
898
|
-
const codeVerifier = await this.storage.get(this.codeVerifierKey(this.clientId));
|
|
899
|
-
if (!codeVerifier) throw new Error("No code verifier found");
|
|
900
|
-
return codeVerifier;
|
|
901
|
-
}
|
|
902
|
-
};
|
|
903
|
-
|
|
904
|
-
//#endregion
|
|
905
|
-
//#region ../agents/dist/src-nFNV3Ttx.js
|
|
1215
|
+
//#region ../agents/dist/src-tXpYCgas.js
|
|
906
1216
|
/**
|
|
907
1217
|
* A generic observability implementation that logs events to the console.
|
|
908
1218
|
*/
|
|
@@ -1021,9 +1331,8 @@ var Agent = class Agent$1 extends Server {
|
|
|
1021
1331
|
super(ctx, env$1);
|
|
1022
1332
|
this._state = DEFAULT_STATE;
|
|
1023
1333
|
this._disposables = new DisposableStore();
|
|
1024
|
-
this.
|
|
1334
|
+
this._destroyed = false;
|
|
1025
1335
|
this._ParentClass = Object.getPrototypeOf(this).constructor;
|
|
1026
|
-
this.mcp = new MCPClientManager(this._ParentClass.name, "0.0.1");
|
|
1027
1336
|
this.initialState = DEFAULT_STATE;
|
|
1028
1337
|
this.observability = genericObservability;
|
|
1029
1338
|
this._flushingQueue = false;
|
|
@@ -1061,27 +1370,37 @@ var Agent = class Agent$1 extends Server {
|
|
|
1061
1370
|
}
|
|
1062
1371
|
});
|
|
1063
1372
|
if (row.type === "cron") {
|
|
1373
|
+
if (this._destroyed) return;
|
|
1064
1374
|
const nextExecutionTime = getNextCronTime(row.cron);
|
|
1065
1375
|
const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1e3);
|
|
1066
1376
|
this.sql`
|
|
1067
1377
|
UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
|
|
1068
1378
|
`;
|
|
1069
|
-
} else
|
|
1379
|
+
} else {
|
|
1380
|
+
if (this._destroyed) return;
|
|
1381
|
+
this.sql`
|
|
1070
1382
|
DELETE FROM cf_agents_schedules WHERE id = ${row.id}
|
|
1071
1383
|
`;
|
|
1384
|
+
}
|
|
1072
1385
|
}
|
|
1386
|
+
if (this._destroyed) return;
|
|
1073
1387
|
await this._scheduleNextAlarm();
|
|
1074
1388
|
};
|
|
1075
1389
|
if (!wrappedClasses.has(this.constructor)) {
|
|
1076
1390
|
this._autoWrapCustomMethods();
|
|
1077
1391
|
wrappedClasses.add(this.constructor);
|
|
1078
1392
|
}
|
|
1079
|
-
this.
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1393
|
+
this.sql`
|
|
1394
|
+
CREATE TABLE IF NOT EXISTS cf_agents_mcp_servers (
|
|
1395
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
1396
|
+
name TEXT NOT NULL,
|
|
1397
|
+
server_url TEXT NOT NULL,
|
|
1398
|
+
callback_url TEXT NOT NULL,
|
|
1399
|
+
client_id TEXT,
|
|
1400
|
+
auth_url TEXT,
|
|
1401
|
+
server_options TEXT
|
|
1402
|
+
)
|
|
1403
|
+
`;
|
|
1085
1404
|
this.sql`
|
|
1086
1405
|
CREATE TABLE IF NOT EXISTS cf_agents_state (
|
|
1087
1406
|
id TEXT PRIMARY KEY NOT NULL,
|
|
@@ -1096,34 +1415,25 @@ var Agent = class Agent$1 extends Server {
|
|
|
1096
1415
|
created_at INTEGER DEFAULT (unixepoch())
|
|
1097
1416
|
)
|
|
1098
1417
|
`;
|
|
1099
|
-
this.ctx.blockConcurrencyWhile(async () => {
|
|
1100
|
-
return this._tryCatch(async () => {
|
|
1101
|
-
this.sql`
|
|
1102
|
-
CREATE TABLE IF NOT EXISTS cf_agents_schedules (
|
|
1103
|
-
id TEXT PRIMARY KEY NOT NULL DEFAULT (randomblob(9)),
|
|
1104
|
-
callback TEXT,
|
|
1105
|
-
payload TEXT,
|
|
1106
|
-
type TEXT NOT NULL CHECK(type IN ('scheduled', 'delayed', 'cron')),
|
|
1107
|
-
time INTEGER,
|
|
1108
|
-
delayInSeconds INTEGER,
|
|
1109
|
-
cron TEXT,
|
|
1110
|
-
created_at INTEGER DEFAULT (unixepoch())
|
|
1111
|
-
)
|
|
1112
|
-
`;
|
|
1113
|
-
await this.alarm();
|
|
1114
|
-
});
|
|
1115
|
-
});
|
|
1116
1418
|
this.sql`
|
|
1117
|
-
CREATE TABLE IF NOT EXISTS
|
|
1118
|
-
id TEXT PRIMARY KEY NOT NULL,
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1419
|
+
CREATE TABLE IF NOT EXISTS cf_agents_schedules (
|
|
1420
|
+
id TEXT PRIMARY KEY NOT NULL DEFAULT (randomblob(9)),
|
|
1421
|
+
callback TEXT,
|
|
1422
|
+
payload TEXT,
|
|
1423
|
+
type TEXT NOT NULL CHECK(type IN ('scheduled', 'delayed', 'cron')),
|
|
1424
|
+
time INTEGER,
|
|
1425
|
+
delayInSeconds INTEGER,
|
|
1426
|
+
cron TEXT,
|
|
1427
|
+
created_at INTEGER DEFAULT (unixepoch())
|
|
1125
1428
|
)
|
|
1126
1429
|
`;
|
|
1430
|
+
this.mcp = new MCPClientManager(this._ParentClass.name, "0.0.1", { storage: this.ctx.storage });
|
|
1431
|
+
this._disposables.add(this.mcp.onServerStateChanged(async () => {
|
|
1432
|
+
this.broadcastMcpServers();
|
|
1433
|
+
}));
|
|
1434
|
+
this._disposables.add(this.mcp.onObservabilityEvent((event) => {
|
|
1435
|
+
this.observability?.emit(event);
|
|
1436
|
+
}));
|
|
1127
1437
|
const _onRequest = this.onRequest.bind(this);
|
|
1128
1438
|
this.onRequest = (request) => {
|
|
1129
1439
|
return agentContext.run({
|
|
@@ -1132,17 +1442,9 @@ var Agent = class Agent$1 extends Server {
|
|
|
1132
1442
|
request,
|
|
1133
1443
|
email: void 0
|
|
1134
1444
|
}, async () => {
|
|
1135
|
-
await this.
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
this.broadcastMcpServers();
|
|
1139
|
-
if (result.authSuccess) this.mcp.establishConnection(result.serverId).catch((error) => {
|
|
1140
|
-
console.error("Background connection failed:", error);
|
|
1141
|
-
}).finally(() => {
|
|
1142
|
-
this.broadcastMcpServers();
|
|
1143
|
-
});
|
|
1144
|
-
return this.handleOAuthCallbackResponse(result, request);
|
|
1145
|
-
}
|
|
1445
|
+
await this.mcp.ensureJsonSchema();
|
|
1446
|
+
const oauthResponse = await this.handleMcpOAuthCallback(request);
|
|
1447
|
+
if (oauthResponse) return oauthResponse;
|
|
1146
1448
|
return this._tryCatch(() => _onRequest(request));
|
|
1147
1449
|
});
|
|
1148
1450
|
};
|
|
@@ -1154,6 +1456,7 @@ var Agent = class Agent$1 extends Server {
|
|
|
1154
1456
|
request: void 0,
|
|
1155
1457
|
email: void 0
|
|
1156
1458
|
}, async () => {
|
|
1459
|
+
await this.mcp.ensureJsonSchema();
|
|
1157
1460
|
if (typeof message !== "string") return this._tryCatch(() => _onMessage(connection, message));
|
|
1158
1461
|
let parsed;
|
|
1159
1462
|
try {
|
|
@@ -1218,7 +1521,7 @@ var Agent = class Agent$1 extends Server {
|
|
|
1218
1521
|
connection,
|
|
1219
1522
|
request: ctx$1.request,
|
|
1220
1523
|
email: void 0
|
|
1221
|
-
}, () => {
|
|
1524
|
+
}, async () => {
|
|
1222
1525
|
if (this.state) connection.send(JSON.stringify({
|
|
1223
1526
|
state: this.state,
|
|
1224
1527
|
type: MessageType.CF_AGENT_STATE
|
|
@@ -1246,7 +1549,7 @@ var Agent = class Agent$1 extends Server {
|
|
|
1246
1549
|
email: void 0
|
|
1247
1550
|
}, async () => {
|
|
1248
1551
|
await this._tryCatch(async () => {
|
|
1249
|
-
await this.
|
|
1552
|
+
await this.mcp.restoreConnectionsFromStorage(this.name);
|
|
1250
1553
|
this.broadcastMcpServers();
|
|
1251
1554
|
return _onStart(props);
|
|
1252
1555
|
});
|
|
@@ -1653,7 +1956,7 @@ var Agent = class Agent$1 extends Server {
|
|
|
1653
1956
|
async _scheduleNextAlarm() {
|
|
1654
1957
|
const result = this.sql`
|
|
1655
1958
|
SELECT time FROM cf_agents_schedules
|
|
1656
|
-
WHERE time
|
|
1959
|
+
WHERE time >= ${Math.floor(Date.now() / 1e3)}
|
|
1657
1960
|
ORDER BY time ASC
|
|
1658
1961
|
LIMIT 1
|
|
1659
1962
|
`;
|
|
@@ -1667,15 +1970,18 @@ var Agent = class Agent$1 extends Server {
|
|
|
1667
1970
|
* Destroy the Agent, removing all state and scheduled tasks
|
|
1668
1971
|
*/
|
|
1669
1972
|
async destroy() {
|
|
1973
|
+
this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
|
|
1670
1974
|
this.sql`DROP TABLE IF EXISTS cf_agents_state`;
|
|
1671
1975
|
this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
|
|
1672
|
-
this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
|
|
1673
1976
|
this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
|
|
1674
1977
|
await this.ctx.storage.deleteAlarm();
|
|
1675
1978
|
await this.ctx.storage.deleteAll();
|
|
1676
1979
|
this._disposables.dispose();
|
|
1677
|
-
await this.mcp.dispose
|
|
1678
|
-
this.
|
|
1980
|
+
await this.mcp.dispose();
|
|
1981
|
+
this._destroyed = true;
|
|
1982
|
+
setTimeout(() => {
|
|
1983
|
+
this.ctx.abort("destroyed");
|
|
1984
|
+
}, 0);
|
|
1679
1985
|
this.observability?.emit({
|
|
1680
1986
|
displayMessage: "Agent destroyed",
|
|
1681
1987
|
id: nanoid(),
|
|
@@ -1691,43 +1997,6 @@ var Agent = class Agent$1 extends Server {
|
|
|
1691
1997
|
_isCallable(method) {
|
|
1692
1998
|
return callableMetadata.has(this[method]);
|
|
1693
1999
|
}
|
|
1694
|
-
async _ensureMcpStateRestored() {
|
|
1695
|
-
if (this._mcpStateRestored) return;
|
|
1696
|
-
this._mcpStateRestored = true;
|
|
1697
|
-
const servers = this.sql`
|
|
1698
|
-
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options
|
|
1699
|
-
FROM cf_agents_mcp_servers
|
|
1700
|
-
`;
|
|
1701
|
-
if (!servers || !Array.isArray(servers) || servers.length === 0) return;
|
|
1702
|
-
for (const server of servers) if (server.callback_url) this.mcp.registerCallbackUrl(`${server.callback_url}/${server.id}`);
|
|
1703
|
-
for (const server of servers) if (!!server.auth_url) {
|
|
1704
|
-
const authProvider = new DurableObjectOAuthClientProvider(this.ctx.storage, this.name, server.callback_url);
|
|
1705
|
-
authProvider.serverId = server.id;
|
|
1706
|
-
if (server.client_id) authProvider.clientId = server.client_id;
|
|
1707
|
-
const parsedOptions = server.server_options ? JSON.parse(server.server_options) : void 0;
|
|
1708
|
-
const conn = new MCPClientConnection(new URL(server.server_url), {
|
|
1709
|
-
name: this.name,
|
|
1710
|
-
version: "1.0.0"
|
|
1711
|
-
}, {
|
|
1712
|
-
client: parsedOptions?.client ?? {},
|
|
1713
|
-
transport: {
|
|
1714
|
-
...parsedOptions?.transport ?? {},
|
|
1715
|
-
type: parsedOptions?.transport?.type ?? "auto",
|
|
1716
|
-
authProvider
|
|
1717
|
-
}
|
|
1718
|
-
});
|
|
1719
|
-
conn.connectionState = "authenticating";
|
|
1720
|
-
this.mcp.mcpConnections[server.id] = conn;
|
|
1721
|
-
} else {
|
|
1722
|
-
const parsedOptions = server.server_options ? JSON.parse(server.server_options) : void 0;
|
|
1723
|
-
this._connectToMcpServerInternal(server.name, server.server_url, server.callback_url, parsedOptions, {
|
|
1724
|
-
id: server.id,
|
|
1725
|
-
oauthClientId: server.client_id ?? void 0
|
|
1726
|
-
}).catch((error) => {
|
|
1727
|
-
console.error(`Error restoring ${server.id}:`, error);
|
|
1728
|
-
});
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
2000
|
/**
|
|
1732
2001
|
* Connect to a new MCP Server
|
|
1733
2002
|
*
|
|
@@ -1736,7 +2005,8 @@ var Agent = class Agent$1 extends Server {
|
|
|
1736
2005
|
* @param callbackHost Base host for the agent, used for the redirect URI. If not provided, will be derived from the current request.
|
|
1737
2006
|
* @param agentsPrefix agents routing prefix if not using `agents`
|
|
1738
2007
|
* @param options MCP client and transport options
|
|
1739
|
-
* @returns authUrl
|
|
2008
|
+
* @returns Server id and state - either "authenticating" with authUrl, or "ready"
|
|
2009
|
+
* @throws If connection or discovery fails
|
|
1740
2010
|
*/
|
|
1741
2011
|
async addMcpServer(serverName, url, callbackHost, agentsPrefix = "agents", options) {
|
|
1742
2012
|
let resolvedCallbackHost = callbackHost;
|
|
@@ -1747,29 +2017,10 @@ var Agent = class Agent$1 extends Server {
|
|
|
1747
2017
|
resolvedCallbackHost = `${requestUrl.protocol}//${requestUrl.host}`;
|
|
1748
2018
|
}
|
|
1749
2019
|
const callbackUrl = `${resolvedCallbackHost}/${agentsPrefix}/${camelCaseToKebabCase(this._ParentClass.name)}/${this.name}/callback`;
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
INSERT
|
|
1753
|
-
OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
|
|
1754
|
-
VALUES (
|
|
1755
|
-
${result.id},
|
|
1756
|
-
${serverName},
|
|
1757
|
-
${url},
|
|
1758
|
-
${result.clientId ?? null},
|
|
1759
|
-
${result.authUrl ?? null},
|
|
1760
|
-
${callbackUrl},
|
|
1761
|
-
${options ? JSON.stringify(options) : null}
|
|
1762
|
-
);
|
|
1763
|
-
`;
|
|
1764
|
-
this.broadcastMcpServers();
|
|
1765
|
-
return result;
|
|
1766
|
-
}
|
|
1767
|
-
async _connectToMcpServerInternal(_serverName, url, callbackUrl, options, reconnect) {
|
|
2020
|
+
await this.mcp.ensureJsonSchema();
|
|
2021
|
+
const id = nanoid(8);
|
|
1768
2022
|
const authProvider = new DurableObjectOAuthClientProvider(this.ctx.storage, this.name, callbackUrl);
|
|
1769
|
-
|
|
1770
|
-
authProvider.serverId = reconnect.id;
|
|
1771
|
-
if (reconnect.oauthClientId) authProvider.clientId = reconnect.oauthClientId;
|
|
1772
|
-
}
|
|
2023
|
+
authProvider.serverId = id;
|
|
1773
2024
|
const transportType = options?.transport?.type ?? "auto";
|
|
1774
2025
|
let headerTransportOpts = {};
|
|
1775
2026
|
if (options?.transport?.headers) headerTransportOpts = {
|
|
@@ -1779,28 +2030,33 @@ var Agent = class Agent$1 extends Server {
|
|
|
1779
2030
|
}) },
|
|
1780
2031
|
requestInit: { headers: options?.transport?.headers }
|
|
1781
2032
|
};
|
|
1782
|
-
|
|
2033
|
+
await this.mcp.registerServer(id, {
|
|
2034
|
+
url,
|
|
2035
|
+
name: serverName,
|
|
2036
|
+
callbackUrl,
|
|
1783
2037
|
client: options?.client,
|
|
1784
|
-
reconnect,
|
|
1785
2038
|
transport: {
|
|
1786
2039
|
...headerTransportOpts,
|
|
1787
2040
|
authProvider,
|
|
1788
2041
|
type: transportType
|
|
1789
2042
|
}
|
|
1790
2043
|
});
|
|
2044
|
+
const result = await this.mcp.connectToServer(id);
|
|
2045
|
+
if (result.state === MCPConnectionState.FAILED) throw new Error(`Failed to connect to MCP server at ${url}: ${result.error}`);
|
|
2046
|
+
if (result.state === MCPConnectionState.AUTHENTICATING) return {
|
|
2047
|
+
id,
|
|
2048
|
+
state: result.state,
|
|
2049
|
+
authUrl: result.authUrl
|
|
2050
|
+
};
|
|
2051
|
+
const discoverResult = await this.mcp.discoverIfConnected(id);
|
|
2052
|
+
if (discoverResult && !discoverResult.success) throw new Error(`Failed to discover MCP server capabilities: ${discoverResult.error}`);
|
|
1791
2053
|
return {
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
id
|
|
2054
|
+
id,
|
|
2055
|
+
state: MCPConnectionState.READY
|
|
1795
2056
|
};
|
|
1796
2057
|
}
|
|
1797
2058
|
async removeMcpServer(id) {
|
|
1798
|
-
this.mcp.
|
|
1799
|
-
this.mcp.unregisterCallbackUrl(id);
|
|
1800
|
-
this.sql`
|
|
1801
|
-
DELETE FROM cf_agents_mcp_servers WHERE id = ${id};
|
|
1802
|
-
`;
|
|
1803
|
-
this.broadcastMcpServers();
|
|
2059
|
+
await this.mcp.removeServer(id);
|
|
1804
2060
|
}
|
|
1805
2061
|
getMcpServers() {
|
|
1806
2062
|
const mcpState = {
|
|
@@ -1809,18 +2065,18 @@ var Agent = class Agent$1 extends Server {
|
|
|
1809
2065
|
servers: {},
|
|
1810
2066
|
tools: this.mcp.listTools()
|
|
1811
2067
|
};
|
|
1812
|
-
const servers = this.
|
|
1813
|
-
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
1814
|
-
`;
|
|
2068
|
+
const servers = this.mcp.listServers();
|
|
1815
2069
|
if (servers && Array.isArray(servers) && servers.length > 0) for (const server of servers) {
|
|
1816
2070
|
const serverConn = this.mcp.mcpConnections[server.id];
|
|
2071
|
+
let defaultState = "not-connected";
|
|
2072
|
+
if (!serverConn && server.auth_url) defaultState = "authenticating";
|
|
1817
2073
|
mcpState.servers[server.id] = {
|
|
1818
2074
|
auth_url: server.auth_url,
|
|
1819
2075
|
capabilities: serverConn?.serverCapabilities ?? null,
|
|
1820
2076
|
instructions: serverConn?.instructions ?? null,
|
|
1821
2077
|
name: server.name,
|
|
1822
2078
|
server_url: server.server_url,
|
|
1823
|
-
state: serverConn?.connectionState ??
|
|
2079
|
+
state: serverConn?.connectionState ?? defaultState
|
|
1824
2080
|
};
|
|
1825
2081
|
}
|
|
1826
2082
|
return mcpState;
|
|
@@ -1832,6 +2088,28 @@ var Agent = class Agent$1 extends Server {
|
|
|
1832
2088
|
}));
|
|
1833
2089
|
}
|
|
1834
2090
|
/**
|
|
2091
|
+
* Handle MCP OAuth callback request if it's an OAuth callback.
|
|
2092
|
+
*
|
|
2093
|
+
* This method encapsulates the entire OAuth callback flow:
|
|
2094
|
+
* 1. Checks if the request is an MCP OAuth callback
|
|
2095
|
+
* 2. Processes the OAuth code exchange
|
|
2096
|
+
* 3. Establishes the connection if successful
|
|
2097
|
+
* 4. Broadcasts MCP server state updates
|
|
2098
|
+
* 5. Returns the appropriate HTTP response
|
|
2099
|
+
*
|
|
2100
|
+
* @param request The incoming HTTP request
|
|
2101
|
+
* @returns Response if this was an OAuth callback, null otherwise
|
|
2102
|
+
*/
|
|
2103
|
+
async handleMcpOAuthCallback(request) {
|
|
2104
|
+
if (!this.mcp.isCallbackRequest(request)) return null;
|
|
2105
|
+
const result = await this.mcp.handleCallbackRequest(request);
|
|
2106
|
+
if (result.authSuccess) this.mcp.establishConnection(result.serverId).catch((error) => {
|
|
2107
|
+
console.error("[Agent handleMcpOAuthCallback] Connection establishment failed:", error);
|
|
2108
|
+
});
|
|
2109
|
+
this.broadcastMcpServers();
|
|
2110
|
+
return this.handleOAuthCallbackResponse(result, request);
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
1835
2113
|
* Handle OAuth callback response using MCPClientManager configuration
|
|
1836
2114
|
* @param result OAuth callback result
|
|
1837
2115
|
* @param request The original request (needed for base URL)
|