hono-agents 2.0.5 → 2.0.7
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 +771 -501
- 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,35 +765,160 @@ 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
|
-
const
|
|
587
|
-
const
|
|
904
|
+
const error = url.searchParams.get("error");
|
|
905
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
906
|
+
if (error) return {
|
|
907
|
+
serverId,
|
|
908
|
+
authSuccess: false,
|
|
909
|
+
authError: errorDescription || error
|
|
910
|
+
};
|
|
588
911
|
if (!code) throw new Error("Unauthorized: no code provided");
|
|
589
912
|
if (!state) throw new Error("Unauthorized: no state provided");
|
|
590
913
|
if (this.mcpConnections[serverId] === void 0) throw new Error(`Could not find serverId: ${serverId}`);
|
|
591
|
-
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"`);
|
|
592
922
|
const conn = this.mcpConnections[serverId];
|
|
593
923
|
if (!conn.options.transport.authProvider) throw new Error("Trying to finalize authentication for a server connection without an authProvider");
|
|
594
924
|
const clientId = conn.options.transport.authProvider.clientId || state;
|
|
@@ -596,21 +926,57 @@ var MCPClientManager = class {
|
|
|
596
926
|
conn.options.transport.authProvider.serverId = serverId;
|
|
597
927
|
try {
|
|
598
928
|
await conn.completeAuthorization(code);
|
|
929
|
+
this.clearServerAuthUrl(serverId);
|
|
930
|
+
this._onServerStateChanged.fire();
|
|
599
931
|
return {
|
|
600
932
|
serverId,
|
|
601
933
|
authSuccess: true
|
|
602
934
|
};
|
|
603
|
-
} catch (error) {
|
|
935
|
+
} catch (error$1) {
|
|
936
|
+
const errorMessage = error$1 instanceof Error ? error$1.message : String(error$1);
|
|
937
|
+
this._onServerStateChanged.fire();
|
|
604
938
|
return {
|
|
605
939
|
serverId,
|
|
606
940
|
authSuccess: false,
|
|
607
|
-
authError:
|
|
941
|
+
authError: errorMessage
|
|
608
942
|
};
|
|
609
943
|
}
|
|
610
944
|
}
|
|
611
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
|
+
/**
|
|
612
978
|
* Establish connection in the background after OAuth completion
|
|
613
|
-
* This method
|
|
979
|
+
* This method connects to the server and discovers its capabilities
|
|
614
980
|
* @param serverId The server ID to establish connection for
|
|
615
981
|
*/
|
|
616
982
|
async establishConnection(serverId) {
|
|
@@ -625,38 +991,34 @@ var MCPClientManager = class {
|
|
|
625
991
|
});
|
|
626
992
|
return;
|
|
627
993
|
}
|
|
628
|
-
|
|
629
|
-
await conn.establishConnection();
|
|
630
|
-
this._onConnected.fire(serverId);
|
|
631
|
-
} catch (error) {
|
|
632
|
-
const url = conn.url.toString();
|
|
994
|
+
if (conn.connectionState === MCPConnectionState.DISCOVERING || conn.connectionState === MCPConnectionState.READY) {
|
|
633
995
|
this._onObservabilityEvent.fire({
|
|
634
996
|
type: "mcp:client:connect",
|
|
635
|
-
displayMessage: `
|
|
997
|
+
displayMessage: `establishConnection skipped for ${serverId}, already in ${conn.connectionState} state`,
|
|
636
998
|
payload: {
|
|
637
|
-
url,
|
|
638
|
-
transport: conn.options.transport.type
|
|
639
|
-
state: conn.connectionState
|
|
640
|
-
error: toErrorMessage(error)
|
|
999
|
+
url: conn.url.toString(),
|
|
1000
|
+
transport: conn.options.transport.type || "unknown",
|
|
1001
|
+
state: conn.connectionState
|
|
641
1002
|
},
|
|
642
1003
|
timestamp: Date.now(),
|
|
643
1004
|
id: nanoid()
|
|
644
1005
|
});
|
|
1006
|
+
return;
|
|
645
1007
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
+
});
|
|
660
1022
|
}
|
|
661
1023
|
/**
|
|
662
1024
|
* Configure OAuth callback handling
|
|
@@ -679,9 +1041,28 @@ var MCPClientManager = class {
|
|
|
679
1041
|
return getNamespacedData(this.mcpConnections, "tools");
|
|
680
1042
|
}
|
|
681
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
|
+
/**
|
|
682
1061
|
* @returns a set of tools that you can use with the AI SDK
|
|
683
1062
|
*/
|
|
684
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.`);
|
|
685
1066
|
return Object.fromEntries(getNamespacedData(this.mcpConnections, "tools").map((tool) => {
|
|
686
1067
|
return [`tool_${tool.serverId.replace(/-/g, "")}_${tool.name}`, {
|
|
687
1068
|
description: tool.description,
|
|
@@ -711,10 +1092,18 @@ var MCPClientManager = class {
|
|
|
711
1092
|
return this.getAITools();
|
|
712
1093
|
}
|
|
713
1094
|
/**
|
|
714
|
-
* 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).
|
|
715
1103
|
*/
|
|
716
1104
|
async closeAllConnections() {
|
|
717
1105
|
const ids = Object.keys(this.mcpConnections);
|
|
1106
|
+
for (const id of ids) this.mcpConnections[id].cancelDiscovery();
|
|
718
1107
|
await Promise.all(ids.map(async (id) => {
|
|
719
1108
|
await this.mcpConnections[id].client.close();
|
|
720
1109
|
}));
|
|
@@ -731,6 +1120,7 @@ var MCPClientManager = class {
|
|
|
731
1120
|
*/
|
|
732
1121
|
async closeConnection(id) {
|
|
733
1122
|
if (!this.mcpConnections[id]) throw new Error(`Connection with id "${id}" does not exist.`);
|
|
1123
|
+
this.mcpConnections[id].cancelDiscovery();
|
|
734
1124
|
await this.mcpConnections[id].client.close();
|
|
735
1125
|
delete this.mcpConnections[id];
|
|
736
1126
|
const store = this._connectionDisposables.get(id);
|
|
@@ -738,13 +1128,29 @@ var MCPClientManager = class {
|
|
|
738
1128
|
this._connectionDisposables.delete(id);
|
|
739
1129
|
}
|
|
740
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
|
+
/**
|
|
741
1147
|
* Dispose the manager and all resources.
|
|
742
1148
|
*/
|
|
743
1149
|
async dispose() {
|
|
744
1150
|
try {
|
|
745
1151
|
await this.closeAllConnections();
|
|
746
1152
|
} finally {
|
|
747
|
-
this.
|
|
1153
|
+
this._onServerStateChanged.dispose();
|
|
748
1154
|
this._onObservabilityEvent.dispose();
|
|
749
1155
|
}
|
|
750
1156
|
}
|
|
@@ -806,96 +1212,7 @@ function getNamespacedData(mcpClients, type) {
|
|
|
806
1212
|
}
|
|
807
1213
|
|
|
808
1214
|
//#endregion
|
|
809
|
-
//#region ../agents/dist/
|
|
810
|
-
var DurableObjectOAuthClientProvider = class {
|
|
811
|
-
constructor(storage, clientName, baseRedirectUrl) {
|
|
812
|
-
this.storage = storage;
|
|
813
|
-
this.clientName = clientName;
|
|
814
|
-
this.baseRedirectUrl = baseRedirectUrl;
|
|
815
|
-
}
|
|
816
|
-
get clientMetadata() {
|
|
817
|
-
return {
|
|
818
|
-
client_name: this.clientName,
|
|
819
|
-
client_uri: this.clientUri,
|
|
820
|
-
grant_types: ["authorization_code", "refresh_token"],
|
|
821
|
-
redirect_uris: [this.redirectUrl],
|
|
822
|
-
response_types: ["code"],
|
|
823
|
-
token_endpoint_auth_method: "none"
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
get clientUri() {
|
|
827
|
-
return new URL(this.redirectUrl).origin;
|
|
828
|
-
}
|
|
829
|
-
get redirectUrl() {
|
|
830
|
-
return `${this.baseRedirectUrl}/${this.serverId}`;
|
|
831
|
-
}
|
|
832
|
-
get clientId() {
|
|
833
|
-
if (!this._clientId_) throw new Error("Trying to access clientId before it was set");
|
|
834
|
-
return this._clientId_;
|
|
835
|
-
}
|
|
836
|
-
set clientId(clientId_) {
|
|
837
|
-
this._clientId_ = clientId_;
|
|
838
|
-
}
|
|
839
|
-
get serverId() {
|
|
840
|
-
if (!this._serverId_) throw new Error("Trying to access serverId before it was set");
|
|
841
|
-
return this._serverId_;
|
|
842
|
-
}
|
|
843
|
-
set serverId(serverId_) {
|
|
844
|
-
this._serverId_ = serverId_;
|
|
845
|
-
}
|
|
846
|
-
keyPrefix(clientId) {
|
|
847
|
-
return `/${this.clientName}/${this.serverId}/${clientId}`;
|
|
848
|
-
}
|
|
849
|
-
clientInfoKey(clientId) {
|
|
850
|
-
return `${this.keyPrefix(clientId)}/client_info/`;
|
|
851
|
-
}
|
|
852
|
-
async clientInformation() {
|
|
853
|
-
if (!this._clientId_) return;
|
|
854
|
-
return await this.storage.get(this.clientInfoKey(this.clientId)) ?? void 0;
|
|
855
|
-
}
|
|
856
|
-
async saveClientInformation(clientInformation) {
|
|
857
|
-
await this.storage.put(this.clientInfoKey(clientInformation.client_id), clientInformation);
|
|
858
|
-
this.clientId = clientInformation.client_id;
|
|
859
|
-
}
|
|
860
|
-
tokenKey(clientId) {
|
|
861
|
-
return `${this.keyPrefix(clientId)}/token`;
|
|
862
|
-
}
|
|
863
|
-
async tokens() {
|
|
864
|
-
if (!this._clientId_) return;
|
|
865
|
-
return await this.storage.get(this.tokenKey(this.clientId)) ?? void 0;
|
|
866
|
-
}
|
|
867
|
-
async saveTokens(tokens) {
|
|
868
|
-
await this.storage.put(this.tokenKey(this.clientId), tokens);
|
|
869
|
-
}
|
|
870
|
-
get authUrl() {
|
|
871
|
-
return this._authUrl_;
|
|
872
|
-
}
|
|
873
|
-
/**
|
|
874
|
-
* Because this operates on the server side (but we need browser auth), we send this url back to the user
|
|
875
|
-
* and require user interact to initiate the redirect flow
|
|
876
|
-
*/
|
|
877
|
-
async redirectToAuthorization(authUrl) {
|
|
878
|
-
const stateToken = nanoid();
|
|
879
|
-
authUrl.searchParams.set("state", stateToken);
|
|
880
|
-
this._authUrl_ = authUrl.toString();
|
|
881
|
-
}
|
|
882
|
-
codeVerifierKey(clientId) {
|
|
883
|
-
return `${this.keyPrefix(clientId)}/code_verifier`;
|
|
884
|
-
}
|
|
885
|
-
async saveCodeVerifier(verifier) {
|
|
886
|
-
const key = this.codeVerifierKey(this.clientId);
|
|
887
|
-
if (await this.storage.get(key)) return;
|
|
888
|
-
await this.storage.put(key, verifier);
|
|
889
|
-
}
|
|
890
|
-
async codeVerifier() {
|
|
891
|
-
const codeVerifier = await this.storage.get(this.codeVerifierKey(this.clientId));
|
|
892
|
-
if (!codeVerifier) throw new Error("No code verifier found");
|
|
893
|
-
return codeVerifier;
|
|
894
|
-
}
|
|
895
|
-
};
|
|
896
|
-
|
|
897
|
-
//#endregion
|
|
898
|
-
//#region ../agents/dist/src-CTtjSFyX.js
|
|
1215
|
+
//#region ../agents/dist/src-tXpYCgas.js
|
|
899
1216
|
/**
|
|
900
1217
|
* A generic observability implementation that logs events to the console.
|
|
901
1218
|
*/
|
|
@@ -1014,8 +1331,8 @@ var Agent = class Agent$1 extends Server {
|
|
|
1014
1331
|
super(ctx, env$1);
|
|
1015
1332
|
this._state = DEFAULT_STATE;
|
|
1016
1333
|
this._disposables = new DisposableStore();
|
|
1334
|
+
this._destroyed = false;
|
|
1017
1335
|
this._ParentClass = Object.getPrototypeOf(this).constructor;
|
|
1018
|
-
this.mcp = new MCPClientManager(this._ParentClass.name, "0.0.1");
|
|
1019
1336
|
this.initialState = DEFAULT_STATE;
|
|
1020
1337
|
this.observability = genericObservability;
|
|
1021
1338
|
this._flushingQueue = false;
|
|
@@ -1053,27 +1370,37 @@ var Agent = class Agent$1 extends Server {
|
|
|
1053
1370
|
}
|
|
1054
1371
|
});
|
|
1055
1372
|
if (row.type === "cron") {
|
|
1373
|
+
if (this._destroyed) return;
|
|
1056
1374
|
const nextExecutionTime = getNextCronTime(row.cron);
|
|
1057
1375
|
const nextTimestamp = Math.floor(nextExecutionTime.getTime() / 1e3);
|
|
1058
1376
|
this.sql`
|
|
1059
1377
|
UPDATE cf_agents_schedules SET time = ${nextTimestamp} WHERE id = ${row.id}
|
|
1060
1378
|
`;
|
|
1061
|
-
} else
|
|
1379
|
+
} else {
|
|
1380
|
+
if (this._destroyed) return;
|
|
1381
|
+
this.sql`
|
|
1062
1382
|
DELETE FROM cf_agents_schedules WHERE id = ${row.id}
|
|
1063
1383
|
`;
|
|
1384
|
+
}
|
|
1064
1385
|
}
|
|
1386
|
+
if (this._destroyed) return;
|
|
1065
1387
|
await this._scheduleNextAlarm();
|
|
1066
1388
|
};
|
|
1067
1389
|
if (!wrappedClasses.has(this.constructor)) {
|
|
1068
1390
|
this._autoWrapCustomMethods();
|
|
1069
1391
|
wrappedClasses.add(this.constructor);
|
|
1070
1392
|
}
|
|
1071
|
-
this.
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
+
`;
|
|
1077
1404
|
this.sql`
|
|
1078
1405
|
CREATE TABLE IF NOT EXISTS cf_agents_state (
|
|
1079
1406
|
id TEXT PRIMARY KEY NOT NULL,
|
|
@@ -1088,34 +1415,25 @@ var Agent = class Agent$1 extends Server {
|
|
|
1088
1415
|
created_at INTEGER DEFAULT (unixepoch())
|
|
1089
1416
|
)
|
|
1090
1417
|
`;
|
|
1091
|
-
this.ctx.blockConcurrencyWhile(async () => {
|
|
1092
|
-
return this._tryCatch(async () => {
|
|
1093
|
-
this.sql`
|
|
1094
|
-
CREATE TABLE IF NOT EXISTS cf_agents_schedules (
|
|
1095
|
-
id TEXT PRIMARY KEY NOT NULL DEFAULT (randomblob(9)),
|
|
1096
|
-
callback TEXT,
|
|
1097
|
-
payload TEXT,
|
|
1098
|
-
type TEXT NOT NULL CHECK(type IN ('scheduled', 'delayed', 'cron')),
|
|
1099
|
-
time INTEGER,
|
|
1100
|
-
delayInSeconds INTEGER,
|
|
1101
|
-
cron TEXT,
|
|
1102
|
-
created_at INTEGER DEFAULT (unixepoch())
|
|
1103
|
-
)
|
|
1104
|
-
`;
|
|
1105
|
-
await this.alarm();
|
|
1106
|
-
});
|
|
1107
|
-
});
|
|
1108
1418
|
this.sql`
|
|
1109
|
-
CREATE TABLE IF NOT EXISTS
|
|
1110
|
-
id TEXT PRIMARY KEY NOT NULL,
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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())
|
|
1117
1428
|
)
|
|
1118
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
|
+
}));
|
|
1119
1437
|
const _onRequest = this.onRequest.bind(this);
|
|
1120
1438
|
this.onRequest = (request) => {
|
|
1121
1439
|
return agentContext.run({
|
|
@@ -1124,8 +1442,9 @@ var Agent = class Agent$1 extends Server {
|
|
|
1124
1442
|
request,
|
|
1125
1443
|
email: void 0
|
|
1126
1444
|
}, async () => {
|
|
1127
|
-
|
|
1128
|
-
|
|
1445
|
+
await this.mcp.ensureJsonSchema();
|
|
1446
|
+
const oauthResponse = await this.handleMcpOAuthCallback(request);
|
|
1447
|
+
if (oauthResponse) return oauthResponse;
|
|
1129
1448
|
return this._tryCatch(() => _onRequest(request));
|
|
1130
1449
|
});
|
|
1131
1450
|
};
|
|
@@ -1137,6 +1456,7 @@ var Agent = class Agent$1 extends Server {
|
|
|
1137
1456
|
request: void 0,
|
|
1138
1457
|
email: void 0
|
|
1139
1458
|
}, async () => {
|
|
1459
|
+
await this.mcp.ensureJsonSchema();
|
|
1140
1460
|
if (typeof message !== "string") return this._tryCatch(() => _onMessage(connection, message));
|
|
1141
1461
|
let parsed;
|
|
1142
1462
|
try {
|
|
@@ -1201,7 +1521,7 @@ var Agent = class Agent$1 extends Server {
|
|
|
1201
1521
|
connection,
|
|
1202
1522
|
request: ctx$1.request,
|
|
1203
1523
|
email: void 0
|
|
1204
|
-
}, () => {
|
|
1524
|
+
}, async () => {
|
|
1205
1525
|
if (this.state) connection.send(JSON.stringify({
|
|
1206
1526
|
state: this.state,
|
|
1207
1527
|
type: MessageType.CF_AGENT_STATE
|
|
@@ -1228,27 +1548,9 @@ var Agent = class Agent$1 extends Server {
|
|
|
1228
1548
|
request: void 0,
|
|
1229
1549
|
email: void 0
|
|
1230
1550
|
}, async () => {
|
|
1231
|
-
await this._tryCatch(() => {
|
|
1232
|
-
|
|
1233
|
-
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
1234
|
-
`;
|
|
1551
|
+
await this._tryCatch(async () => {
|
|
1552
|
+
await this.mcp.restoreConnectionsFromStorage(this.name);
|
|
1235
1553
|
this.broadcastMcpServers();
|
|
1236
|
-
if (servers && Array.isArray(servers) && servers.length > 0) {
|
|
1237
|
-
servers.forEach((server) => {
|
|
1238
|
-
if (server.callback_url) this.mcp.registerCallbackUrl(`${server.callback_url}/${server.id}`);
|
|
1239
|
-
});
|
|
1240
|
-
servers.forEach((server) => {
|
|
1241
|
-
this._connectToMcpServerInternal(server.name, server.server_url, server.callback_url, server.server_options ? JSON.parse(server.server_options) : void 0, {
|
|
1242
|
-
id: server.id,
|
|
1243
|
-
oauthClientId: server.client_id ?? void 0
|
|
1244
|
-
}).then(() => {
|
|
1245
|
-
this.broadcastMcpServers();
|
|
1246
|
-
}).catch((error) => {
|
|
1247
|
-
console.error(`Error connecting to MCP server: ${server.name} (${server.server_url})`, error);
|
|
1248
|
-
this.broadcastMcpServers();
|
|
1249
|
-
});
|
|
1250
|
-
});
|
|
1251
|
-
}
|
|
1252
1554
|
return _onStart(props);
|
|
1253
1555
|
});
|
|
1254
1556
|
});
|
|
@@ -1654,7 +1956,7 @@ var Agent = class Agent$1 extends Server {
|
|
|
1654
1956
|
async _scheduleNextAlarm() {
|
|
1655
1957
|
const result = this.sql`
|
|
1656
1958
|
SELECT time FROM cf_agents_schedules
|
|
1657
|
-
WHERE time
|
|
1959
|
+
WHERE time >= ${Math.floor(Date.now() / 1e3)}
|
|
1658
1960
|
ORDER BY time ASC
|
|
1659
1961
|
LIMIT 1
|
|
1660
1962
|
`;
|
|
@@ -1668,15 +1970,18 @@ var Agent = class Agent$1 extends Server {
|
|
|
1668
1970
|
* Destroy the Agent, removing all state and scheduled tasks
|
|
1669
1971
|
*/
|
|
1670
1972
|
async destroy() {
|
|
1973
|
+
this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
|
|
1671
1974
|
this.sql`DROP TABLE IF EXISTS cf_agents_state`;
|
|
1672
1975
|
this.sql`DROP TABLE IF EXISTS cf_agents_schedules`;
|
|
1673
|
-
this.sql`DROP TABLE IF EXISTS cf_agents_mcp_servers`;
|
|
1674
1976
|
this.sql`DROP TABLE IF EXISTS cf_agents_queues`;
|
|
1675
1977
|
await this.ctx.storage.deleteAlarm();
|
|
1676
1978
|
await this.ctx.storage.deleteAll();
|
|
1677
1979
|
this._disposables.dispose();
|
|
1678
|
-
await this.mcp.dispose
|
|
1679
|
-
this.
|
|
1980
|
+
await this.mcp.dispose();
|
|
1981
|
+
this._destroyed = true;
|
|
1982
|
+
setTimeout(() => {
|
|
1983
|
+
this.ctx.abort("destroyed");
|
|
1984
|
+
}, 0);
|
|
1680
1985
|
this.observability?.emit({
|
|
1681
1986
|
displayMessage: "Agent destroyed",
|
|
1682
1987
|
id: nanoid(),
|
|
@@ -1700,7 +2005,8 @@ var Agent = class Agent$1 extends Server {
|
|
|
1700
2005
|
* @param callbackHost Base host for the agent, used for the redirect URI. If not provided, will be derived from the current request.
|
|
1701
2006
|
* @param agentsPrefix agents routing prefix if not using `agents`
|
|
1702
2007
|
* @param options MCP client and transport options
|
|
1703
|
-
* @returns authUrl
|
|
2008
|
+
* @returns Server id and state - either "authenticating" with authUrl, or "ready"
|
|
2009
|
+
* @throws If connection or discovery fails
|
|
1704
2010
|
*/
|
|
1705
2011
|
async addMcpServer(serverName, url, callbackHost, agentsPrefix = "agents", options) {
|
|
1706
2012
|
let resolvedCallbackHost = callbackHost;
|
|
@@ -1711,89 +2017,10 @@ var Agent = class Agent$1 extends Server {
|
|
|
1711
2017
|
resolvedCallbackHost = `${requestUrl.protocol}//${requestUrl.host}`;
|
|
1712
2018
|
}
|
|
1713
2019
|
const callbackUrl = `${resolvedCallbackHost}/${agentsPrefix}/${camelCaseToKebabCase(this._ParentClass.name)}/${this.name}/callback`;
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
INSERT OR REPLACE INTO cf_agents_mcp_servers (id, name, server_url, client_id, auth_url, callback_url, server_options)
|
|
1717
|
-
VALUES (
|
|
1718
|
-
${serverId},
|
|
1719
|
-
${serverName},
|
|
1720
|
-
${url},
|
|
1721
|
-
${null},
|
|
1722
|
-
${null},
|
|
1723
|
-
${callbackUrl},
|
|
1724
|
-
${options ? JSON.stringify(options) : null}
|
|
1725
|
-
);
|
|
1726
|
-
`;
|
|
1727
|
-
const result = await this._connectToMcpServerInternal(serverName, url, callbackUrl, options, { id: serverId });
|
|
1728
|
-
if (result.clientId || result.authUrl) this.sql`
|
|
1729
|
-
UPDATE cf_agents_mcp_servers
|
|
1730
|
-
SET client_id = ${result.clientId ?? null}, auth_url = ${result.authUrl ?? null}
|
|
1731
|
-
WHERE id = ${serverId}
|
|
1732
|
-
`;
|
|
1733
|
-
this.broadcastMcpServers();
|
|
1734
|
-
return result;
|
|
1735
|
-
}
|
|
1736
|
-
/**
|
|
1737
|
-
* Handle potential OAuth callback requests after DO hibernation.
|
|
1738
|
-
* Detects OAuth callbacks, restores state from database, and processes the callback.
|
|
1739
|
-
* Returns a Response if this was an OAuth callback, otherwise returns undefined.
|
|
1740
|
-
*/
|
|
1741
|
-
async _handlePotentialOAuthCallback(request) {
|
|
1742
|
-
if (request.method !== "GET") return;
|
|
1743
|
-
const url = new URL(request.url);
|
|
1744
|
-
if (!(url.pathname.includes("/callback/") && url.searchParams.has("code"))) return;
|
|
1745
|
-
const pathParts = url.pathname.split("/");
|
|
1746
|
-
const callbackIndex = pathParts.indexOf("callback");
|
|
1747
|
-
const serverId = callbackIndex !== -1 ? pathParts[callbackIndex + 1] : null;
|
|
1748
|
-
if (!serverId) return new Response("Invalid callback URL: missing serverId", { status: 400 });
|
|
1749
|
-
if (this.mcp.isCallbackRequest(request) && this.mcp.mcpConnections[serverId]) return this._processOAuthCallback(request);
|
|
1750
|
-
try {
|
|
1751
|
-
const server = this.sql`
|
|
1752
|
-
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options
|
|
1753
|
-
FROM cf_agents_mcp_servers
|
|
1754
|
-
WHERE id = ${serverId}
|
|
1755
|
-
`.find((s) => s.id === serverId);
|
|
1756
|
-
if (!server) return new Response(`OAuth callback failed: Server ${serverId} not found in database`, { status: 404 });
|
|
1757
|
-
if (!server.callback_url) return new Response(`OAuth callback failed: No callback URL stored for server ${serverId}`, { status: 500 });
|
|
1758
|
-
this.mcp.registerCallbackUrl(`${server.callback_url}/${server.id}`);
|
|
1759
|
-
if (!this.mcp.mcpConnections[serverId]) {
|
|
1760
|
-
let parsedOptions;
|
|
1761
|
-
try {
|
|
1762
|
-
parsedOptions = server.server_options ? JSON.parse(server.server_options) : void 0;
|
|
1763
|
-
} catch {
|
|
1764
|
-
return new Response(`OAuth callback failed: Invalid server options in database for ${serverId}`, { status: 500 });
|
|
1765
|
-
}
|
|
1766
|
-
await this._connectToMcpServerInternal(server.name, server.server_url, server.callback_url, parsedOptions, {
|
|
1767
|
-
id: server.id,
|
|
1768
|
-
oauthClientId: server.client_id ?? void 0
|
|
1769
|
-
});
|
|
1770
|
-
}
|
|
1771
|
-
return this._processOAuthCallback(request);
|
|
1772
|
-
} catch (error) {
|
|
1773
|
-
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
1774
|
-
console.error(`Failed to restore MCP state for ${serverId}:`, error);
|
|
1775
|
-
return new Response(`OAuth callback failed during state restoration: ${errorMsg}`, { status: 500 });
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
/**
|
|
1779
|
-
* Process an OAuth callback request (assumes state is already restored)
|
|
1780
|
-
*/
|
|
1781
|
-
async _processOAuthCallback(request) {
|
|
1782
|
-
const result = await this.mcp.handleCallbackRequest(request);
|
|
1783
|
-
this.broadcastMcpServers();
|
|
1784
|
-
if (result.authSuccess) this.mcp.establishConnection(result.serverId).catch((error) => {
|
|
1785
|
-
console.error("Background connection failed:", error);
|
|
1786
|
-
}).finally(() => {
|
|
1787
|
-
this.broadcastMcpServers();
|
|
1788
|
-
});
|
|
1789
|
-
return this.handleOAuthCallbackResponse(result, request);
|
|
1790
|
-
}
|
|
1791
|
-
async _connectToMcpServerInternal(_serverName, url, callbackUrl, options, reconnect) {
|
|
2020
|
+
await this.mcp.ensureJsonSchema();
|
|
2021
|
+
const id = nanoid(8);
|
|
1792
2022
|
const authProvider = new DurableObjectOAuthClientProvider(this.ctx.storage, this.name, callbackUrl);
|
|
1793
|
-
|
|
1794
|
-
authProvider.serverId = reconnect.id;
|
|
1795
|
-
if (reconnect.oauthClientId) authProvider.clientId = reconnect.oauthClientId;
|
|
1796
|
-
}
|
|
2023
|
+
authProvider.serverId = id;
|
|
1797
2024
|
const transportType = options?.transport?.type ?? "auto";
|
|
1798
2025
|
let headerTransportOpts = {};
|
|
1799
2026
|
if (options?.transport?.headers) headerTransportOpts = {
|
|
@@ -1803,28 +2030,33 @@ var Agent = class Agent$1 extends Server {
|
|
|
1803
2030
|
}) },
|
|
1804
2031
|
requestInit: { headers: options?.transport?.headers }
|
|
1805
2032
|
};
|
|
1806
|
-
|
|
2033
|
+
await this.mcp.registerServer(id, {
|
|
2034
|
+
url,
|
|
2035
|
+
name: serverName,
|
|
2036
|
+
callbackUrl,
|
|
1807
2037
|
client: options?.client,
|
|
1808
|
-
reconnect,
|
|
1809
2038
|
transport: {
|
|
1810
2039
|
...headerTransportOpts,
|
|
1811
2040
|
authProvider,
|
|
1812
2041
|
type: transportType
|
|
1813
2042
|
}
|
|
1814
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}`);
|
|
1815
2053
|
return {
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
id
|
|
2054
|
+
id,
|
|
2055
|
+
state: MCPConnectionState.READY
|
|
1819
2056
|
};
|
|
1820
2057
|
}
|
|
1821
2058
|
async removeMcpServer(id) {
|
|
1822
|
-
this.mcp.
|
|
1823
|
-
this.mcp.unregisterCallbackUrl(id);
|
|
1824
|
-
this.sql`
|
|
1825
|
-
DELETE FROM cf_agents_mcp_servers WHERE id = ${id};
|
|
1826
|
-
`;
|
|
1827
|
-
this.broadcastMcpServers();
|
|
2059
|
+
await this.mcp.removeServer(id);
|
|
1828
2060
|
}
|
|
1829
2061
|
getMcpServers() {
|
|
1830
2062
|
const mcpState = {
|
|
@@ -1833,18 +2065,18 @@ var Agent = class Agent$1 extends Server {
|
|
|
1833
2065
|
servers: {},
|
|
1834
2066
|
tools: this.mcp.listTools()
|
|
1835
2067
|
};
|
|
1836
|
-
const servers = this.
|
|
1837
|
-
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers;
|
|
1838
|
-
`;
|
|
2068
|
+
const servers = this.mcp.listServers();
|
|
1839
2069
|
if (servers && Array.isArray(servers) && servers.length > 0) for (const server of servers) {
|
|
1840
2070
|
const serverConn = this.mcp.mcpConnections[server.id];
|
|
2071
|
+
let defaultState = "not-connected";
|
|
2072
|
+
if (!serverConn && server.auth_url) defaultState = "authenticating";
|
|
1841
2073
|
mcpState.servers[server.id] = {
|
|
1842
2074
|
auth_url: server.auth_url,
|
|
1843
2075
|
capabilities: serverConn?.serverCapabilities ?? null,
|
|
1844
2076
|
instructions: serverConn?.instructions ?? null,
|
|
1845
2077
|
name: server.name,
|
|
1846
2078
|
server_url: server.server_url,
|
|
1847
|
-
state: serverConn?.connectionState ??
|
|
2079
|
+
state: serverConn?.connectionState ?? defaultState
|
|
1848
2080
|
};
|
|
1849
2081
|
}
|
|
1850
2082
|
return mcpState;
|
|
@@ -1856,6 +2088,28 @@ var Agent = class Agent$1 extends Server {
|
|
|
1856
2088
|
}));
|
|
1857
2089
|
}
|
|
1858
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
|
+
/**
|
|
1859
2113
|
* Handle OAuth callback response using MCPClientManager configuration
|
|
1860
2114
|
* @param result OAuth callback result
|
|
1861
2115
|
* @param request The original request (needed for base URL)
|
|
@@ -1864,10 +2118,21 @@ var Agent = class Agent$1 extends Server {
|
|
|
1864
2118
|
handleOAuthCallbackResponse(result, request) {
|
|
1865
2119
|
const config = this.mcp.getOAuthCallbackConfig();
|
|
1866
2120
|
if (config?.customHandler) return config.customHandler(result);
|
|
1867
|
-
|
|
1868
|
-
if (config?.
|
|
1869
|
-
|
|
1870
|
-
|
|
2121
|
+
const baseOrigin = new URL(request.url).origin;
|
|
2122
|
+
if (config?.successRedirect && result.authSuccess) try {
|
|
2123
|
+
return Response.redirect(new URL(config.successRedirect, baseOrigin).href);
|
|
2124
|
+
} catch (e) {
|
|
2125
|
+
console.error("Invalid successRedirect URL:", config.successRedirect, e);
|
|
2126
|
+
return Response.redirect(baseOrigin);
|
|
2127
|
+
}
|
|
2128
|
+
if (config?.errorRedirect && !result.authSuccess) try {
|
|
2129
|
+
const errorUrl = `${config.errorRedirect}?error=${encodeURIComponent(result.authError || "Unknown error")}`;
|
|
2130
|
+
return Response.redirect(new URL(errorUrl, baseOrigin).href);
|
|
2131
|
+
} catch (e) {
|
|
2132
|
+
console.error("Invalid errorRedirect URL:", config.errorRedirect, e);
|
|
2133
|
+
return Response.redirect(baseOrigin);
|
|
2134
|
+
}
|
|
2135
|
+
return Response.redirect(baseOrigin);
|
|
1871
2136
|
}
|
|
1872
2137
|
};
|
|
1873
2138
|
const wrappedClasses = /* @__PURE__ */ new Set();
|
|
@@ -1893,10 +2158,15 @@ async function routeAgentRequest(request, env$1, options) {
|
|
|
1893
2158
|
prefix: "agents",
|
|
1894
2159
|
...options
|
|
1895
2160
|
});
|
|
1896
|
-
if (response && corsHeaders && request.headers.get("upgrade")?.toLowerCase() !== "websocket" && request.headers.get("Upgrade")?.toLowerCase() !== "websocket")
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2161
|
+
if (response && corsHeaders && request.headers.get("upgrade")?.toLowerCase() !== "websocket" && request.headers.get("Upgrade")?.toLowerCase() !== "websocket") {
|
|
2162
|
+
const newHeaders = new Headers(response.headers);
|
|
2163
|
+
for (const [key, value] of Object.entries(corsHeaders)) newHeaders.set(key, value);
|
|
2164
|
+
response = new Response(response.body, {
|
|
2165
|
+
status: response.status,
|
|
2166
|
+
statusText: response.statusText,
|
|
2167
|
+
headers: newHeaders
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
1900
2170
|
return response;
|
|
1901
2171
|
}
|
|
1902
2172
|
/**
|