hono-agents 0.0.0-cccbd0f → 0.0.0-cd8b7fd
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 +509 -303
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import "partysocket";
|
|
2
3
|
import { nanoid } from "nanoid";
|
|
3
4
|
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker-provider.js";
|
|
@@ -5,14 +6,17 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
5
6
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
6
7
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
7
8
|
import { ElicitRequestSchema, PromptListChangedNotificationSchema, ResourceListChangedNotificationSchema, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9
9
|
import { parseCronExpression } from "cron-schedule";
|
|
10
10
|
import "cloudflare:email";
|
|
11
11
|
import { Server, routePartykitRequest } from "partyserver";
|
|
12
12
|
import { env } from "hono/adapter";
|
|
13
13
|
import { createMiddleware } from "hono/factory";
|
|
14
14
|
|
|
15
|
-
//#region ../agents/dist/
|
|
15
|
+
//#region ../agents/dist/context-BkKbAa1R.js
|
|
16
|
+
const agentContext = new AsyncLocalStorage();
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region ../agents/dist/ai-types-CwgHzwUb.js
|
|
16
20
|
/**
|
|
17
21
|
* Enum for message types to improve type safety and maintainability
|
|
18
22
|
*/
|
|
@@ -34,7 +38,7 @@ let MessageType = /* @__PURE__ */ function(MessageType$1) {
|
|
|
34
38
|
}({});
|
|
35
39
|
|
|
36
40
|
//#endregion
|
|
37
|
-
//#region ../agents/dist/client-
|
|
41
|
+
//#region ../agents/dist/client-CcyhkGfN.js
|
|
38
42
|
/**
|
|
39
43
|
* Convert a camelCase string to a kebab-case string
|
|
40
44
|
* @param str The string to convert
|
|
@@ -48,12 +52,14 @@ function camelCaseToKebabCase(str) {
|
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
//#endregion
|
|
51
|
-
//#region ../agents/dist/do-oauth-client-provider-
|
|
55
|
+
//#region ../agents/dist/do-oauth-client-provider-B1fVIshX.js
|
|
56
|
+
const STATE_EXPIRATION_MS = 600 * 1e3;
|
|
52
57
|
var DurableObjectOAuthClientProvider = class {
|
|
53
58
|
constructor(storage, clientName, baseRedirectUrl) {
|
|
54
59
|
this.storage = storage;
|
|
55
60
|
this.clientName = clientName;
|
|
56
61
|
this.baseRedirectUrl = baseRedirectUrl;
|
|
62
|
+
if (!storage) throw new Error("DurableObjectOAuthClientProvider requires a valid DurableObjectStorage instance");
|
|
57
63
|
}
|
|
58
64
|
get clientMetadata() {
|
|
59
65
|
return {
|
|
@@ -69,7 +75,7 @@ var DurableObjectOAuthClientProvider = class {
|
|
|
69
75
|
return new URL(this.redirectUrl).origin;
|
|
70
76
|
}
|
|
71
77
|
get redirectUrl() {
|
|
72
|
-
return
|
|
78
|
+
return this.baseRedirectUrl;
|
|
73
79
|
}
|
|
74
80
|
get clientId() {
|
|
75
81
|
if (!this._clientId_) throw new Error("Trying to access clientId before it was set");
|
|
@@ -112,15 +118,72 @@ var DurableObjectOAuthClientProvider = class {
|
|
|
112
118
|
get authUrl() {
|
|
113
119
|
return this._authUrl_;
|
|
114
120
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
121
|
+
stateKey(nonce) {
|
|
122
|
+
return `/${this.clientName}/${this.serverId}/state/${nonce}`;
|
|
123
|
+
}
|
|
124
|
+
async state() {
|
|
125
|
+
const nonce = nanoid();
|
|
126
|
+
const state = `${nonce}.${this.serverId}`;
|
|
127
|
+
const storedState = {
|
|
128
|
+
nonce,
|
|
129
|
+
serverId: this.serverId,
|
|
130
|
+
createdAt: Date.now()
|
|
131
|
+
};
|
|
132
|
+
await this.storage.put(this.stateKey(nonce), storedState);
|
|
133
|
+
return state;
|
|
134
|
+
}
|
|
135
|
+
async checkState(state) {
|
|
136
|
+
const parts = state.split(".");
|
|
137
|
+
if (parts.length !== 2) return {
|
|
138
|
+
valid: false,
|
|
139
|
+
error: "Invalid state format"
|
|
140
|
+
};
|
|
141
|
+
const [nonce, serverId] = parts;
|
|
142
|
+
const key = this.stateKey(nonce);
|
|
143
|
+
const storedState = await this.storage.get(key);
|
|
144
|
+
if (!storedState) return {
|
|
145
|
+
valid: false,
|
|
146
|
+
error: "State not found or already used"
|
|
147
|
+
};
|
|
148
|
+
if (storedState.serverId !== serverId) {
|
|
149
|
+
await this.storage.delete(key);
|
|
150
|
+
return {
|
|
151
|
+
valid: false,
|
|
152
|
+
error: "State serverId mismatch"
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (Date.now() - storedState.createdAt > STATE_EXPIRATION_MS) {
|
|
156
|
+
await this.storage.delete(key);
|
|
157
|
+
return {
|
|
158
|
+
valid: false,
|
|
159
|
+
error: "State expired"
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
valid: true,
|
|
164
|
+
serverId
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
async consumeState(state) {
|
|
168
|
+
const parts = state.split(".");
|
|
169
|
+
if (parts.length !== 2) {
|
|
170
|
+
console.warn(`[OAuth] consumeState called with invalid state format: ${state.substring(0, 20)}...`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const [nonce] = parts;
|
|
174
|
+
await this.storage.delete(this.stateKey(nonce));
|
|
175
|
+
}
|
|
176
|
+
async redirectToAuthorization(authUrl) {
|
|
122
177
|
this._authUrl_ = authUrl.toString();
|
|
123
178
|
}
|
|
179
|
+
async invalidateCredentials(scope) {
|
|
180
|
+
if (!this._clientId_) return;
|
|
181
|
+
const deleteKeys = [];
|
|
182
|
+
if (scope === "all" || scope === "client") deleteKeys.push(this.clientInfoKey(this.clientId));
|
|
183
|
+
if (scope === "all" || scope === "tokens") deleteKeys.push(this.tokenKey(this.clientId));
|
|
184
|
+
if (scope === "all" || scope === "verifier") deleteKeys.push(this.codeVerifierKey(this.clientId));
|
|
185
|
+
if (deleteKeys.length > 0) await this.storage.delete(deleteKeys);
|
|
186
|
+
}
|
|
124
187
|
codeVerifierKey(clientId) {
|
|
125
188
|
return `${this.keyPrefix(clientId)}/code_verifier`;
|
|
126
189
|
}
|
|
@@ -134,10 +197,13 @@ var DurableObjectOAuthClientProvider = class {
|
|
|
134
197
|
if (!codeVerifier) throw new Error("No code verifier found");
|
|
135
198
|
return codeVerifier;
|
|
136
199
|
}
|
|
200
|
+
async deleteCodeVerifier() {
|
|
201
|
+
await this.storage.delete(this.codeVerifierKey(this.clientId));
|
|
202
|
+
}
|
|
137
203
|
};
|
|
138
204
|
|
|
139
205
|
//#endregion
|
|
140
|
-
//#region ../agents/dist/client-
|
|
206
|
+
//#region ../agents/dist/client-QZa2Rq0l.js
|
|
141
207
|
function toDisposable(fn) {
|
|
142
208
|
return { dispose: fn };
|
|
143
209
|
}
|
|
@@ -185,6 +251,22 @@ function isTransportNotImplemented(error) {
|
|
|
185
251
|
const msg = toErrorMessage(error);
|
|
186
252
|
return msg.includes("404") || msg.includes("405") || msg.includes("Not Implemented") || msg.includes("not implemented");
|
|
187
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Connection state machine for MCP client connections.
|
|
256
|
+
*
|
|
257
|
+
* State transitions:
|
|
258
|
+
* - Non-OAuth: init() → CONNECTING → DISCOVERING → READY
|
|
259
|
+
* - OAuth: init() → AUTHENTICATING → (callback) → CONNECTING → DISCOVERING → READY
|
|
260
|
+
* - Any state can transition to FAILED on error
|
|
261
|
+
*/
|
|
262
|
+
const MCPConnectionState = {
|
|
263
|
+
AUTHENTICATING: "authenticating",
|
|
264
|
+
CONNECTING: "connecting",
|
|
265
|
+
CONNECTED: "connected",
|
|
266
|
+
DISCOVERING: "discovering",
|
|
267
|
+
READY: "ready",
|
|
268
|
+
FAILED: "failed"
|
|
269
|
+
};
|
|
188
270
|
var MCPClientConnection = class {
|
|
189
271
|
constructor(url, info, options = {
|
|
190
272
|
client: {},
|
|
@@ -192,7 +274,7 @@ var MCPClientConnection = class {
|
|
|
192
274
|
}) {
|
|
193
275
|
this.url = url;
|
|
194
276
|
this.options = options;
|
|
195
|
-
this.connectionState =
|
|
277
|
+
this.connectionState = MCPConnectionState.CONNECTING;
|
|
196
278
|
this.tools = [];
|
|
197
279
|
this.prompts = [];
|
|
198
280
|
this.resources = [];
|
|
@@ -208,36 +290,49 @@ var MCPClientConnection = class {
|
|
|
208
290
|
});
|
|
209
291
|
}
|
|
210
292
|
/**
|
|
211
|
-
* Initialize a client connection
|
|
293
|
+
* Initialize a client connection, if authentication is required, the connection will be in the AUTHENTICATING state
|
|
294
|
+
* Sets connection state based on the result and emits observability events
|
|
212
295
|
*
|
|
213
|
-
* @returns
|
|
296
|
+
* @returns Error message if connection failed, undefined otherwise
|
|
214
297
|
*/
|
|
215
298
|
async init() {
|
|
216
299
|
const transportType = this.options.transport.type;
|
|
217
300
|
if (!transportType) throw new Error("Transport type must be specified");
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.
|
|
223
|
-
|
|
224
|
-
|
|
301
|
+
const res = await this.tryConnect(transportType);
|
|
302
|
+
this.connectionState = res.state;
|
|
303
|
+
if (res.state === MCPConnectionState.CONNECTED && res.transport) {
|
|
304
|
+
this.client.setRequestHandler(ElicitRequestSchema, async (request) => {
|
|
305
|
+
return await this.handleElicitationRequest(request);
|
|
306
|
+
});
|
|
307
|
+
this.lastConnectedTransport = res.transport;
|
|
308
|
+
this._onObservabilityEvent.fire({
|
|
309
|
+
type: "mcp:client:connect",
|
|
310
|
+
displayMessage: `Connected successfully using ${res.transport} transport for ${this.url.toString()}`,
|
|
311
|
+
payload: {
|
|
312
|
+
url: this.url.toString(),
|
|
313
|
+
transport: res.transport,
|
|
314
|
+
state: this.connectionState
|
|
315
|
+
},
|
|
316
|
+
timestamp: Date.now(),
|
|
317
|
+
id: nanoid()
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
} else if (res.state === MCPConnectionState.FAILED && res.error) {
|
|
321
|
+
const errorMessage = toErrorMessage(res.error);
|
|
225
322
|
this._onObservabilityEvent.fire({
|
|
226
323
|
type: "mcp:client:connect",
|
|
227
|
-
displayMessage: `
|
|
324
|
+
displayMessage: `Failed to connect to ${this.url.toString()}: ${errorMessage}`,
|
|
228
325
|
payload: {
|
|
229
326
|
url: this.url.toString(),
|
|
230
327
|
transport: transportType,
|
|
231
328
|
state: this.connectionState,
|
|
232
|
-
error:
|
|
329
|
+
error: errorMessage
|
|
233
330
|
},
|
|
234
331
|
timestamp: Date.now(),
|
|
235
332
|
id: nanoid()
|
|
236
333
|
});
|
|
237
|
-
|
|
238
|
-
return;
|
|
334
|
+
return errorMessage;
|
|
239
335
|
}
|
|
240
|
-
await this.discoverAndRegister();
|
|
241
336
|
}
|
|
242
337
|
/**
|
|
243
338
|
* Finish OAuth by probing transports based on configured type.
|
|
@@ -269,113 +364,189 @@ var MCPClientConnection = class {
|
|
|
269
364
|
* Complete OAuth authorization
|
|
270
365
|
*/
|
|
271
366
|
async completeAuthorization(code) {
|
|
272
|
-
if (this.connectionState !==
|
|
367
|
+
if (this.connectionState !== MCPConnectionState.AUTHENTICATING) throw new Error("Connection must be in authenticating state to complete authorization");
|
|
273
368
|
try {
|
|
274
369
|
await this.finishAuthProbe(code);
|
|
275
|
-
this.connectionState =
|
|
370
|
+
this.connectionState = MCPConnectionState.CONNECTING;
|
|
276
371
|
} catch (error) {
|
|
277
|
-
this.connectionState =
|
|
372
|
+
this.connectionState = MCPConnectionState.FAILED;
|
|
278
373
|
throw error;
|
|
279
374
|
}
|
|
280
375
|
}
|
|
281
376
|
/**
|
|
282
|
-
*
|
|
377
|
+
* Discover server capabilities and register tools, resources, prompts, and templates.
|
|
378
|
+
* This method does the work but does not manage connection state - that's handled by discover().
|
|
283
379
|
*/
|
|
284
|
-
async
|
|
285
|
-
|
|
380
|
+
async discoverAndRegister() {
|
|
381
|
+
this.serverCapabilities = this.client.getServerCapabilities();
|
|
382
|
+
if (!this.serverCapabilities) throw new Error("The MCP Server failed to return server capabilities");
|
|
383
|
+
const operations = [];
|
|
384
|
+
const operationNames = [];
|
|
385
|
+
operations.push(Promise.resolve(this.client.getInstructions()));
|
|
386
|
+
operationNames.push("instructions");
|
|
387
|
+
if (this.serverCapabilities.tools) {
|
|
388
|
+
operations.push(this.registerTools());
|
|
389
|
+
operationNames.push("tools");
|
|
390
|
+
}
|
|
391
|
+
if (this.serverCapabilities.resources) {
|
|
392
|
+
operations.push(this.registerResources());
|
|
393
|
+
operationNames.push("resources");
|
|
394
|
+
}
|
|
395
|
+
if (this.serverCapabilities.prompts) {
|
|
396
|
+
operations.push(this.registerPrompts());
|
|
397
|
+
operationNames.push("prompts");
|
|
398
|
+
}
|
|
399
|
+
if (this.serverCapabilities.resources) {
|
|
400
|
+
operations.push(this.registerResourceTemplates());
|
|
401
|
+
operationNames.push("resource templates");
|
|
402
|
+
}
|
|
286
403
|
try {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
404
|
+
const results = await Promise.all(operations);
|
|
405
|
+
for (let i = 0; i < results.length; i++) {
|
|
406
|
+
const result = results[i];
|
|
407
|
+
switch (operationNames[i]) {
|
|
408
|
+
case "instructions":
|
|
409
|
+
this.instructions = result;
|
|
410
|
+
break;
|
|
411
|
+
case "tools":
|
|
412
|
+
this.tools = result;
|
|
413
|
+
break;
|
|
414
|
+
case "resources":
|
|
415
|
+
this.resources = result;
|
|
416
|
+
break;
|
|
417
|
+
case "prompts":
|
|
418
|
+
this.prompts = result;
|
|
419
|
+
break;
|
|
420
|
+
case "resource templates":
|
|
421
|
+
this.resourceTemplates = result;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
291
425
|
} catch (error) {
|
|
292
|
-
this.
|
|
426
|
+
this._onObservabilityEvent.fire({
|
|
427
|
+
type: "mcp:client:discover",
|
|
428
|
+
displayMessage: `Failed to discover capabilities for ${this.url.toString()}: ${toErrorMessage(error)}`,
|
|
429
|
+
payload: {
|
|
430
|
+
url: this.url.toString(),
|
|
431
|
+
error: toErrorMessage(error)
|
|
432
|
+
},
|
|
433
|
+
timestamp: Date.now(),
|
|
434
|
+
id: nanoid()
|
|
435
|
+
});
|
|
293
436
|
throw error;
|
|
294
437
|
}
|
|
295
438
|
}
|
|
296
439
|
/**
|
|
297
|
-
* Discover server capabilities
|
|
440
|
+
* Discover server capabilities with timeout and cancellation support.
|
|
441
|
+
* If called while a previous discovery is in-flight, the previous discovery will be aborted.
|
|
442
|
+
*
|
|
443
|
+
* @param options Optional configuration
|
|
444
|
+
* @param options.timeoutMs Timeout in milliseconds (default: 15000)
|
|
445
|
+
* @returns Result indicating success/failure with optional error message
|
|
298
446
|
*/
|
|
299
|
-
async
|
|
300
|
-
|
|
301
|
-
this.
|
|
302
|
-
if (!this.serverCapabilities) throw new Error("The MCP Server failed to return server capabilities");
|
|
303
|
-
const [instructionsResult, toolsResult, resourcesResult, promptsResult, resourceTemplatesResult] = await Promise.allSettled([
|
|
304
|
-
this.client.getInstructions(),
|
|
305
|
-
this.registerTools(),
|
|
306
|
-
this.registerResources(),
|
|
307
|
-
this.registerPrompts(),
|
|
308
|
-
this.registerResourceTemplates()
|
|
309
|
-
]);
|
|
310
|
-
const operations = [
|
|
311
|
-
{
|
|
312
|
-
name: "instructions",
|
|
313
|
-
result: instructionsResult
|
|
314
|
-
},
|
|
315
|
-
{
|
|
316
|
-
name: "tools",
|
|
317
|
-
result: toolsResult
|
|
318
|
-
},
|
|
319
|
-
{
|
|
320
|
-
name: "resources",
|
|
321
|
-
result: resourcesResult
|
|
322
|
-
},
|
|
323
|
-
{
|
|
324
|
-
name: "prompts",
|
|
325
|
-
result: promptsResult
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
name: "resource templates",
|
|
329
|
-
result: resourceTemplatesResult
|
|
330
|
-
}
|
|
331
|
-
];
|
|
332
|
-
for (const { name, result } of operations) if (result.status === "rejected") {
|
|
333
|
-
const url = this.url.toString();
|
|
447
|
+
async discover(options = {}) {
|
|
448
|
+
const { timeoutMs = 15e3 } = options;
|
|
449
|
+
if (this.connectionState !== MCPConnectionState.CONNECTED && this.connectionState !== MCPConnectionState.READY) {
|
|
334
450
|
this._onObservabilityEvent.fire({
|
|
335
451
|
type: "mcp:client:discover",
|
|
336
|
-
displayMessage: `
|
|
452
|
+
displayMessage: `Discovery skipped for ${this.url.toString()}, state is ${this.connectionState}`,
|
|
337
453
|
payload: {
|
|
338
|
-
url,
|
|
339
|
-
|
|
340
|
-
error: result.reason
|
|
454
|
+
url: this.url.toString(),
|
|
455
|
+
state: this.connectionState
|
|
341
456
|
},
|
|
342
457
|
timestamp: Date.now(),
|
|
343
458
|
id: nanoid()
|
|
344
459
|
});
|
|
460
|
+
return {
|
|
461
|
+
success: false,
|
|
462
|
+
error: `Discovery skipped - connection in ${this.connectionState} state`
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (this._discoveryAbortController) {
|
|
466
|
+
this._discoveryAbortController.abort();
|
|
467
|
+
this._discoveryAbortController = void 0;
|
|
468
|
+
}
|
|
469
|
+
const abortController = new AbortController();
|
|
470
|
+
this._discoveryAbortController = abortController;
|
|
471
|
+
this.connectionState = MCPConnectionState.DISCOVERING;
|
|
472
|
+
let timeoutId;
|
|
473
|
+
try {
|
|
474
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
475
|
+
timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error(`Discovery timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
476
|
+
});
|
|
477
|
+
if (abortController.signal.aborted) throw new Error("Discovery was cancelled");
|
|
478
|
+
const abortPromise = new Promise((_, reject) => {
|
|
479
|
+
abortController.signal.addEventListener("abort", () => {
|
|
480
|
+
reject(/* @__PURE__ */ new Error("Discovery was cancelled"));
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
await Promise.race([
|
|
484
|
+
this.discoverAndRegister(),
|
|
485
|
+
timeoutPromise,
|
|
486
|
+
abortPromise
|
|
487
|
+
]);
|
|
488
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
489
|
+
this.connectionState = MCPConnectionState.READY;
|
|
490
|
+
this._onObservabilityEvent.fire({
|
|
491
|
+
type: "mcp:client:discover",
|
|
492
|
+
displayMessage: `Discovery completed for ${this.url.toString()}`,
|
|
493
|
+
payload: { url: this.url.toString() },
|
|
494
|
+
timestamp: Date.now(),
|
|
495
|
+
id: nanoid()
|
|
496
|
+
});
|
|
497
|
+
return { success: true };
|
|
498
|
+
} catch (e) {
|
|
499
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
500
|
+
this.connectionState = MCPConnectionState.CONNECTED;
|
|
501
|
+
return {
|
|
502
|
+
success: false,
|
|
503
|
+
error: e instanceof Error ? e.message : String(e)
|
|
504
|
+
};
|
|
505
|
+
} finally {
|
|
506
|
+
this._discoveryAbortController = void 0;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Cancel any in-flight discovery operation.
|
|
511
|
+
* Called when closing the connection.
|
|
512
|
+
*/
|
|
513
|
+
cancelDiscovery() {
|
|
514
|
+
if (this._discoveryAbortController) {
|
|
515
|
+
this._discoveryAbortController.abort();
|
|
516
|
+
this._discoveryAbortController = void 0;
|
|
345
517
|
}
|
|
346
|
-
this.instructions = instructionsResult.status === "fulfilled" ? instructionsResult.value : void 0;
|
|
347
|
-
this.tools = toolsResult.status === "fulfilled" ? toolsResult.value : [];
|
|
348
|
-
this.resources = resourcesResult.status === "fulfilled" ? resourcesResult.value : [];
|
|
349
|
-
this.prompts = promptsResult.status === "fulfilled" ? promptsResult.value : [];
|
|
350
|
-
this.resourceTemplates = resourceTemplatesResult.status === "fulfilled" ? resourceTemplatesResult.value : [];
|
|
351
|
-
this.connectionState = "ready";
|
|
352
518
|
}
|
|
353
519
|
/**
|
|
354
|
-
* Notification handler registration
|
|
520
|
+
* Notification handler registration for tools
|
|
521
|
+
* Should only be called if serverCapabilities.tools exists
|
|
355
522
|
*/
|
|
356
523
|
async registerTools() {
|
|
357
|
-
if (
|
|
358
|
-
if (this.serverCapabilities.tools.listChanged) this.client.setNotificationHandler(ToolListChangedNotificationSchema, async (_notification) => {
|
|
524
|
+
if (this.serverCapabilities?.tools?.listChanged) this.client.setNotificationHandler(ToolListChangedNotificationSchema, async (_notification) => {
|
|
359
525
|
this.tools = await this.fetchTools();
|
|
360
526
|
});
|
|
361
527
|
return this.fetchTools();
|
|
362
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* Notification handler registration for resources
|
|
531
|
+
* Should only be called if serverCapabilities.resources exists
|
|
532
|
+
*/
|
|
363
533
|
async registerResources() {
|
|
364
|
-
if (
|
|
365
|
-
if (this.serverCapabilities.resources.listChanged) this.client.setNotificationHandler(ResourceListChangedNotificationSchema, async (_notification) => {
|
|
534
|
+
if (this.serverCapabilities?.resources?.listChanged) this.client.setNotificationHandler(ResourceListChangedNotificationSchema, async (_notification) => {
|
|
366
535
|
this.resources = await this.fetchResources();
|
|
367
536
|
});
|
|
368
537
|
return this.fetchResources();
|
|
369
538
|
}
|
|
539
|
+
/**
|
|
540
|
+
* Notification handler registration for prompts
|
|
541
|
+
* Should only be called if serverCapabilities.prompts exists
|
|
542
|
+
*/
|
|
370
543
|
async registerPrompts() {
|
|
371
|
-
if (
|
|
372
|
-
if (this.serverCapabilities.prompts.listChanged) this.client.setNotificationHandler(PromptListChangedNotificationSchema, async (_notification) => {
|
|
544
|
+
if (this.serverCapabilities?.prompts?.listChanged) this.client.setNotificationHandler(PromptListChangedNotificationSchema, async (_notification) => {
|
|
373
545
|
this.prompts = await this.fetchPrompts();
|
|
374
546
|
});
|
|
375
547
|
return this.fetchPrompts();
|
|
376
548
|
}
|
|
377
549
|
async registerResourceTemplates() {
|
|
378
|
-
if (!this.serverCapabilities || !this.serverCapabilities.resources) return [];
|
|
379
550
|
return this.fetchResourceTemplates();
|
|
380
551
|
}
|
|
381
552
|
async fetchTools() {
|
|
@@ -441,44 +612,24 @@ var MCPClientConnection = class {
|
|
|
441
612
|
const transport = this.getTransport(currentTransportType);
|
|
442
613
|
try {
|
|
443
614
|
await this.client.connect(transport);
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
displayMessage: `Connected successfully using ${currentTransportType} transport for ${url}`,
|
|
449
|
-
payload: {
|
|
450
|
-
url,
|
|
451
|
-
transport: currentTransportType,
|
|
452
|
-
state: this.connectionState
|
|
453
|
-
},
|
|
454
|
-
timestamp: Date.now(),
|
|
455
|
-
id: nanoid()
|
|
456
|
-
});
|
|
457
|
-
break;
|
|
615
|
+
return {
|
|
616
|
+
state: MCPConnectionState.CONNECTED,
|
|
617
|
+
transport: currentTransportType
|
|
618
|
+
};
|
|
458
619
|
} catch (e) {
|
|
459
620
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
460
|
-
if (isUnauthorized(error))
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
payload: {
|
|
467
|
-
url,
|
|
468
|
-
transport: currentTransportType,
|
|
469
|
-
state: this.connectionState
|
|
470
|
-
},
|
|
471
|
-
timestamp: Date.now(),
|
|
472
|
-
id: nanoid()
|
|
473
|
-
});
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
throw e;
|
|
621
|
+
if (isUnauthorized(error)) return { state: MCPConnectionState.AUTHENTICATING };
|
|
622
|
+
if (isTransportNotImplemented(error) && hasFallback) continue;
|
|
623
|
+
return {
|
|
624
|
+
state: MCPConnectionState.FAILED,
|
|
625
|
+
error
|
|
626
|
+
};
|
|
477
627
|
}
|
|
478
628
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
629
|
+
return {
|
|
630
|
+
state: MCPConnectionState.FAILED,
|
|
631
|
+
error: /* @__PURE__ */ new Error("No transports available")
|
|
632
|
+
};
|
|
482
633
|
}
|
|
483
634
|
_capabilityErrorHandler(empty, method) {
|
|
484
635
|
return (e) => {
|
|
@@ -522,13 +673,32 @@ var MCPClientManager = class {
|
|
|
522
673
|
this.onObservabilityEvent = this._onObservabilityEvent.event;
|
|
523
674
|
this._onServerStateChanged = new Emitter();
|
|
524
675
|
this.onServerStateChanged = this._onServerStateChanged.event;
|
|
676
|
+
if (!options.storage) throw new Error("MCPClientManager requires a valid DurableObjectStorage instance");
|
|
525
677
|
this._storage = options.storage;
|
|
526
678
|
}
|
|
679
|
+
sql(query, ...bindings) {
|
|
680
|
+
return [...this._storage.sql.exec(query, ...bindings)];
|
|
681
|
+
}
|
|
682
|
+
saveServerToStorage(server) {
|
|
683
|
+
this.sql(`INSERT OR REPLACE INTO cf_agents_mcp_servers (
|
|
684
|
+
id, name, server_url, client_id, auth_url, callback_url, server_options
|
|
685
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`, server.id, server.name, server.server_url, server.client_id ?? null, server.auth_url ?? null, server.callback_url, server.server_options ?? null);
|
|
686
|
+
}
|
|
687
|
+
removeServerFromStorage(serverId) {
|
|
688
|
+
this.sql("DELETE FROM cf_agents_mcp_servers WHERE id = ?", serverId);
|
|
689
|
+
}
|
|
690
|
+
getServersFromStorage() {
|
|
691
|
+
return this.sql("SELECT id, name, server_url, client_id, auth_url, callback_url, server_options FROM cf_agents_mcp_servers");
|
|
692
|
+
}
|
|
693
|
+
clearServerAuthUrl(serverId) {
|
|
694
|
+
this.sql("UPDATE cf_agents_mcp_servers SET auth_url = NULL WHERE id = ?", serverId);
|
|
695
|
+
}
|
|
527
696
|
/**
|
|
528
697
|
* Create an auth provider for a server
|
|
529
698
|
* @internal
|
|
530
699
|
*/
|
|
531
700
|
createAuthProvider(serverId, callbackUrl, clientName, clientId) {
|
|
701
|
+
if (!this._storage) throw new Error("Cannot create auth provider: storage is not initialized");
|
|
532
702
|
const authProvider = new DurableObjectOAuthClientProvider(this._storage, clientName, callbackUrl);
|
|
533
703
|
authProvider.serverId = serverId;
|
|
534
704
|
if (clientId) authProvider.clientId = clientId;
|
|
@@ -542,7 +712,7 @@ var MCPClientManager = class {
|
|
|
542
712
|
*/
|
|
543
713
|
async restoreConnectionsFromStorage(clientName) {
|
|
544
714
|
if (this._isRestored) return;
|
|
545
|
-
const servers =
|
|
715
|
+
const servers = this.getServersFromStorage();
|
|
546
716
|
if (!servers || servers.length === 0) {
|
|
547
717
|
this._isRestored = true;
|
|
548
718
|
return;
|
|
@@ -550,12 +720,12 @@ var MCPClientManager = class {
|
|
|
550
720
|
for (const server of servers) {
|
|
551
721
|
const existingConn = this.mcpConnections[server.id];
|
|
552
722
|
if (existingConn) {
|
|
553
|
-
if (existingConn.connectionState ===
|
|
723
|
+
if (existingConn.connectionState === MCPConnectionState.READY) {
|
|
554
724
|
console.warn(`[MCPClientManager] Server ${server.id} already has a ready connection. Skipping recreation.`);
|
|
555
725
|
continue;
|
|
556
726
|
}
|
|
557
|
-
if (existingConn.connectionState ===
|
|
558
|
-
if (existingConn.connectionState ===
|
|
727
|
+
if (existingConn.connectionState === MCPConnectionState.AUTHENTICATING || existingConn.connectionState === MCPConnectionState.CONNECTING || existingConn.connectionState === MCPConnectionState.DISCOVERING) continue;
|
|
728
|
+
if (existingConn.connectionState === MCPConnectionState.FAILED) {
|
|
559
729
|
try {
|
|
560
730
|
await existingConn.client.close();
|
|
561
731
|
} catch (error) {
|
|
@@ -568,7 +738,7 @@ var MCPClientManager = class {
|
|
|
568
738
|
}
|
|
569
739
|
const parsedOptions = server.server_options ? JSON.parse(server.server_options) : null;
|
|
570
740
|
const authProvider = this.createAuthProvider(server.id, server.callback_url, clientName, server.client_id ?? void 0);
|
|
571
|
-
this.createConnection(server.id, server.server_url, {
|
|
741
|
+
const conn = this.createConnection(server.id, server.server_url, {
|
|
572
742
|
client: parsedOptions?.client ?? {},
|
|
573
743
|
transport: {
|
|
574
744
|
...parsedOptions?.transport ?? {},
|
|
@@ -576,13 +746,27 @@ var MCPClientManager = class {
|
|
|
576
746
|
authProvider
|
|
577
747
|
}
|
|
578
748
|
});
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
749
|
+
if (server.auth_url) {
|
|
750
|
+
conn.connectionState = MCPConnectionState.AUTHENTICATING;
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
this._restoreServer(server.id);
|
|
582
754
|
}
|
|
583
755
|
this._isRestored = true;
|
|
584
756
|
}
|
|
585
757
|
/**
|
|
758
|
+
* Internal method to restore a single server connection and discovery
|
|
759
|
+
*/
|
|
760
|
+
async _restoreServer(serverId) {
|
|
761
|
+
if ((await this.connectToServer(serverId).catch((error) => {
|
|
762
|
+
console.error(`Error connecting to ${serverId}:`, error);
|
|
763
|
+
return null;
|
|
764
|
+
}))?.state === MCPConnectionState.CONNECTED) {
|
|
765
|
+
const discoverResult = await this.discoverIfConnected(serverId);
|
|
766
|
+
if (discoverResult && !discoverResult.success) console.error(`Error discovering ${serverId}:`, discoverResult.error);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
586
770
|
* Connect to and register an MCP server
|
|
587
771
|
*
|
|
588
772
|
* @deprecated This method is maintained for backward compatibility.
|
|
@@ -629,7 +813,7 @@ var MCPClientManager = class {
|
|
|
629
813
|
await this.mcpConnections[id].init();
|
|
630
814
|
if (options.reconnect?.oauthCode) try {
|
|
631
815
|
await this.mcpConnections[id].completeAuthorization(options.reconnect.oauthCode);
|
|
632
|
-
await this.mcpConnections[id].
|
|
816
|
+
await this.mcpConnections[id].init();
|
|
633
817
|
} catch (error) {
|
|
634
818
|
this._onObservabilityEvent.fire({
|
|
635
819
|
type: "mcp:client:connect",
|
|
@@ -646,19 +830,22 @@ var MCPClientManager = class {
|
|
|
646
830
|
throw error;
|
|
647
831
|
}
|
|
648
832
|
const authUrl = options.transport?.authProvider?.authUrl;
|
|
649
|
-
if (this.mcpConnections[id].connectionState ===
|
|
833
|
+
if (this.mcpConnections[id].connectionState === MCPConnectionState.AUTHENTICATING && authUrl && options.transport?.authProvider?.redirectUrl) return {
|
|
650
834
|
authUrl,
|
|
651
835
|
clientId: options.transport?.authProvider?.clientId,
|
|
652
836
|
id
|
|
653
837
|
};
|
|
838
|
+
const discoverResult = await this.discoverIfConnected(id);
|
|
839
|
+
if (discoverResult && !discoverResult.success) throw new Error(`Failed to discover server capabilities: ${discoverResult.error}`);
|
|
654
840
|
return { id };
|
|
655
841
|
}
|
|
656
842
|
/**
|
|
657
843
|
* Create an in-memory connection object and set up observability
|
|
658
844
|
* Does NOT save to storage - use registerServer() for that
|
|
845
|
+
* @returns The connection object (existing or newly created)
|
|
659
846
|
*/
|
|
660
847
|
createConnection(id, url, options) {
|
|
661
|
-
if (this.mcpConnections[id]) return;
|
|
848
|
+
if (this.mcpConnections[id]) return this.mcpConnections[id];
|
|
662
849
|
const normalizedTransport = {
|
|
663
850
|
...options.transport,
|
|
664
851
|
type: options.transport?.type ?? "auto"
|
|
@@ -680,6 +867,7 @@ var MCPClientManager = class {
|
|
|
680
867
|
store.add(this.mcpConnections[id].onObservabilityEvent((event) => {
|
|
681
868
|
this._onObservabilityEvent.fire(event);
|
|
682
869
|
}));
|
|
870
|
+
return this.mcpConnections[id];
|
|
683
871
|
}
|
|
684
872
|
/**
|
|
685
873
|
* Register an MCP server connection without connecting
|
|
@@ -697,7 +885,8 @@ var MCPClientManager = class {
|
|
|
697
885
|
type: options.transport?.type ?? "auto"
|
|
698
886
|
}
|
|
699
887
|
});
|
|
700
|
-
|
|
888
|
+
const { authProvider: _, ...transportWithoutAuth } = options.transport ?? {};
|
|
889
|
+
this.saveServerToStorage({
|
|
701
890
|
id,
|
|
702
891
|
name: options.name,
|
|
703
892
|
server_url: options.url,
|
|
@@ -706,7 +895,7 @@ var MCPClientManager = class {
|
|
|
706
895
|
auth_url: options.authUrl ?? null,
|
|
707
896
|
server_options: JSON.stringify({
|
|
708
897
|
client: options.client,
|
|
709
|
-
transport:
|
|
898
|
+
transport: transportWithoutAuth
|
|
710
899
|
})
|
|
711
900
|
});
|
|
712
901
|
this._onServerStateChanged.fire();
|
|
@@ -715,14 +904,13 @@ var MCPClientManager = class {
|
|
|
715
904
|
/**
|
|
716
905
|
* Connect to an already registered MCP server and initialize the connection.
|
|
717
906
|
*
|
|
718
|
-
* For OAuth servers,
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
* For non-OAuth servers, this establishes the connection immediately and returns
|
|
723
|
-
* `{ state: "ready" }`.
|
|
907
|
+
* For OAuth servers, returns `{ state: "authenticating", authUrl, clientId? }`.
|
|
908
|
+
* The user must complete the OAuth flow via the authUrl, which triggers a
|
|
909
|
+
* callback handled by `handleCallbackRequest()`.
|
|
724
910
|
*
|
|
725
|
-
*
|
|
911
|
+
* For non-OAuth servers, establishes the transport connection and returns
|
|
912
|
+
* `{ state: "connected" }`. Call `discoverIfConnected()` afterwards to
|
|
913
|
+
* discover capabilities and transition to "ready" state.
|
|
726
914
|
*
|
|
727
915
|
* @param id Server ID (must be registered first via registerServer())
|
|
728
916
|
* @returns Connection result with current state and OAuth info (if applicable)
|
|
@@ -730,70 +918,99 @@ var MCPClientManager = class {
|
|
|
730
918
|
async connectToServer(id) {
|
|
731
919
|
const conn = this.mcpConnections[id];
|
|
732
920
|
if (!conn) throw new Error(`Server ${id} is not registered. Call registerServer() first.`);
|
|
733
|
-
await conn.init();
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
clientId
|
|
921
|
+
const error = await conn.init();
|
|
922
|
+
this._onServerStateChanged.fire();
|
|
923
|
+
switch (conn.connectionState) {
|
|
924
|
+
case MCPConnectionState.FAILED: return {
|
|
925
|
+
state: conn.connectionState,
|
|
926
|
+
error: error ?? "Unknown connection error"
|
|
927
|
+
};
|
|
928
|
+
case MCPConnectionState.AUTHENTICATING: {
|
|
929
|
+
const authUrl = conn.options.transport.authProvider?.authUrl;
|
|
930
|
+
const redirectUrl = conn.options.transport.authProvider?.redirectUrl;
|
|
931
|
+
if (!authUrl || !redirectUrl) return {
|
|
932
|
+
state: MCPConnectionState.FAILED,
|
|
933
|
+
error: `OAuth configuration incomplete: missing ${!authUrl ? "authUrl" : "redirectUrl"}`
|
|
934
|
+
};
|
|
935
|
+
const clientId = conn.options.transport.authProvider?.clientId;
|
|
936
|
+
const serverRow = this.getServersFromStorage().find((s) => s.id === id);
|
|
937
|
+
if (serverRow) {
|
|
938
|
+
this.saveServerToStorage({
|
|
939
|
+
...serverRow,
|
|
940
|
+
auth_url: authUrl,
|
|
941
|
+
client_id: clientId ?? null
|
|
942
|
+
});
|
|
943
|
+
this._onServerStateChanged.fire();
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
state: conn.connectionState,
|
|
947
|
+
authUrl,
|
|
948
|
+
clientId
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
case MCPConnectionState.CONNECTED: return { state: conn.connectionState };
|
|
952
|
+
default: return {
|
|
953
|
+
state: MCPConnectionState.FAILED,
|
|
954
|
+
error: `Unexpected connection state after init: ${conn.connectionState}`
|
|
748
955
|
};
|
|
749
956
|
}
|
|
750
|
-
if (conn.connectionState === "ready") this._onServerStateChanged.fire();
|
|
751
|
-
return { state: "ready" };
|
|
752
957
|
}
|
|
753
|
-
|
|
958
|
+
extractServerIdFromState(state) {
|
|
959
|
+
if (!state) return null;
|
|
960
|
+
const parts = state.split(".");
|
|
961
|
+
return parts.length === 2 ? parts[1] : null;
|
|
962
|
+
}
|
|
963
|
+
isCallbackRequest(req) {
|
|
754
964
|
if (req.method !== "GET") return false;
|
|
755
965
|
if (!req.url.includes("/callback")) return false;
|
|
756
|
-
|
|
966
|
+
const state = new URL(req.url).searchParams.get("state");
|
|
967
|
+
const serverId = this.extractServerIdFromState(state);
|
|
968
|
+
if (!serverId) return false;
|
|
969
|
+
return this.getServersFromStorage().some((server) => server.id === serverId);
|
|
757
970
|
}
|
|
758
971
|
async handleCallbackRequest(req) {
|
|
759
972
|
const url = new URL(req.url);
|
|
760
|
-
const matchingServer = (await this._storage.listServers()).find((server) => {
|
|
761
|
-
return server.callback_url && req.url.startsWith(server.callback_url);
|
|
762
|
-
});
|
|
763
|
-
if (!matchingServer) throw new Error(`No callback URI match found for the request url: ${req.url}. Was the request matched with \`isCallbackRequest()\`?`);
|
|
764
|
-
const serverId = matchingServer.id;
|
|
765
973
|
const code = url.searchParams.get("code");
|
|
766
974
|
const state = url.searchParams.get("state");
|
|
767
975
|
const error = url.searchParams.get("error");
|
|
768
976
|
const errorDescription = url.searchParams.get("error_description");
|
|
977
|
+
if (!state) throw new Error("Unauthorized: no state provided");
|
|
978
|
+
const serverId = this.extractServerIdFromState(state);
|
|
979
|
+
if (!serverId) throw new Error("No serverId found in state parameter. Expected format: {nonce}.{serverId}");
|
|
980
|
+
if (!this.getServersFromStorage().some((server) => server.id === serverId)) throw new Error(`No server found with id "${serverId}". Was the request matched with \`isCallbackRequest()\`?`);
|
|
981
|
+
if (this.mcpConnections[serverId] === void 0) throw new Error(`Could not find serverId: ${serverId}`);
|
|
982
|
+
const conn = this.mcpConnections[serverId];
|
|
983
|
+
if (!conn.options.transport.authProvider) throw new Error("Trying to finalize authentication for a server connection without an authProvider");
|
|
984
|
+
const authProvider = conn.options.transport.authProvider;
|
|
985
|
+
authProvider.serverId = serverId;
|
|
986
|
+
const stateValidation = await authProvider.checkState(state);
|
|
987
|
+
if (!stateValidation.valid) throw new Error(`Invalid state: ${stateValidation.error}`);
|
|
769
988
|
if (error) return {
|
|
770
989
|
serverId,
|
|
771
990
|
authSuccess: false,
|
|
772
991
|
authError: errorDescription || error
|
|
773
992
|
};
|
|
774
993
|
if (!code) throw new Error("Unauthorized: no code provided");
|
|
775
|
-
if (
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
if (!conn.options.transport.authProvider) throw new Error("Trying to finalize authentication for a server connection without an authProvider");
|
|
784
|
-
const clientId = conn.options.transport.authProvider.clientId || state;
|
|
785
|
-
conn.options.transport.authProvider.clientId = clientId;
|
|
786
|
-
conn.options.transport.authProvider.serverId = serverId;
|
|
994
|
+
if (this.mcpConnections[serverId].connectionState === MCPConnectionState.READY || this.mcpConnections[serverId].connectionState === MCPConnectionState.CONNECTED) {
|
|
995
|
+
this.clearServerAuthUrl(serverId);
|
|
996
|
+
return {
|
|
997
|
+
serverId,
|
|
998
|
+
authSuccess: true
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
if (this.mcpConnections[serverId].connectionState !== MCPConnectionState.AUTHENTICATING) throw new Error(`Failed to authenticate: the client is in "${this.mcpConnections[serverId].connectionState}" state, expected "authenticating"`);
|
|
787
1002
|
try {
|
|
1003
|
+
await authProvider.consumeState(state);
|
|
788
1004
|
await conn.completeAuthorization(code);
|
|
789
|
-
await
|
|
1005
|
+
await authProvider.deleteCodeVerifier();
|
|
1006
|
+
this.clearServerAuthUrl(serverId);
|
|
790
1007
|
this._onServerStateChanged.fire();
|
|
791
1008
|
return {
|
|
792
1009
|
serverId,
|
|
793
1010
|
authSuccess: true
|
|
794
1011
|
};
|
|
795
|
-
} catch (
|
|
796
|
-
const errorMessage =
|
|
1012
|
+
} catch (authError) {
|
|
1013
|
+
const errorMessage = authError instanceof Error ? authError.message : String(authError);
|
|
797
1014
|
this._onServerStateChanged.fire();
|
|
798
1015
|
return {
|
|
799
1016
|
serverId,
|
|
@@ -803,8 +1020,40 @@ var MCPClientManager = class {
|
|
|
803
1020
|
}
|
|
804
1021
|
}
|
|
805
1022
|
/**
|
|
1023
|
+
* Discover server capabilities if connection is in CONNECTED or READY state.
|
|
1024
|
+
* Transitions to DISCOVERING then READY (or CONNECTED on error).
|
|
1025
|
+
* Can be called to refresh server capabilities (e.g., from a UI refresh button).
|
|
1026
|
+
*
|
|
1027
|
+
* If called while a previous discovery is in-flight for the same server,
|
|
1028
|
+
* the previous discovery will be aborted.
|
|
1029
|
+
*
|
|
1030
|
+
* @param serverId The server ID to discover
|
|
1031
|
+
* @param options Optional configuration
|
|
1032
|
+
* @param options.timeoutMs Timeout in milliseconds (default: 30000)
|
|
1033
|
+
* @returns Result with current state and optional error, or undefined if connection not found
|
|
1034
|
+
*/
|
|
1035
|
+
async discoverIfConnected(serverId, options = {}) {
|
|
1036
|
+
const conn = this.mcpConnections[serverId];
|
|
1037
|
+
if (!conn) {
|
|
1038
|
+
this._onObservabilityEvent.fire({
|
|
1039
|
+
type: "mcp:client:discover",
|
|
1040
|
+
displayMessage: `Connection not found for ${serverId}`,
|
|
1041
|
+
payload: {},
|
|
1042
|
+
timestamp: Date.now(),
|
|
1043
|
+
id: nanoid()
|
|
1044
|
+
});
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const result = await conn.discover(options);
|
|
1048
|
+
this._onServerStateChanged.fire();
|
|
1049
|
+
return {
|
|
1050
|
+
...result,
|
|
1051
|
+
state: conn.connectionState
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
806
1055
|
* Establish connection in the background after OAuth completion
|
|
807
|
-
* This method
|
|
1056
|
+
* This method connects to the server and discovers its capabilities
|
|
808
1057
|
* @param serverId The server ID to establish connection for
|
|
809
1058
|
*/
|
|
810
1059
|
async establishConnection(serverId) {
|
|
@@ -819,25 +1068,34 @@ var MCPClientManager = class {
|
|
|
819
1068
|
});
|
|
820
1069
|
return;
|
|
821
1070
|
}
|
|
822
|
-
|
|
823
|
-
await conn.establishConnection();
|
|
824
|
-
this._onServerStateChanged.fire();
|
|
825
|
-
} catch (error) {
|
|
826
|
-
const url = conn.url.toString();
|
|
1071
|
+
if (conn.connectionState === MCPConnectionState.DISCOVERING || conn.connectionState === MCPConnectionState.READY) {
|
|
827
1072
|
this._onObservabilityEvent.fire({
|
|
828
1073
|
type: "mcp:client:connect",
|
|
829
|
-
displayMessage: `
|
|
1074
|
+
displayMessage: `establishConnection skipped for ${serverId}, already in ${conn.connectionState} state`,
|
|
830
1075
|
payload: {
|
|
831
|
-
url,
|
|
832
|
-
transport: conn.options.transport.type
|
|
833
|
-
state: conn.connectionState
|
|
834
|
-
error: toErrorMessage(error)
|
|
1076
|
+
url: conn.url.toString(),
|
|
1077
|
+
transport: conn.options.transport.type || "unknown",
|
|
1078
|
+
state: conn.connectionState
|
|
835
1079
|
},
|
|
836
1080
|
timestamp: Date.now(),
|
|
837
1081
|
id: nanoid()
|
|
838
1082
|
});
|
|
839
|
-
|
|
1083
|
+
return;
|
|
840
1084
|
}
|
|
1085
|
+
const connectResult = await this.connectToServer(serverId);
|
|
1086
|
+
this._onServerStateChanged.fire();
|
|
1087
|
+
if (connectResult.state === MCPConnectionState.CONNECTED) await this.discoverIfConnected(serverId);
|
|
1088
|
+
this._onObservabilityEvent.fire({
|
|
1089
|
+
type: "mcp:client:connect",
|
|
1090
|
+
displayMessage: `establishConnection completed for ${serverId}, final state: ${conn.connectionState}`,
|
|
1091
|
+
payload: {
|
|
1092
|
+
url: conn.url.toString(),
|
|
1093
|
+
transport: conn.options.transport.type || "unknown",
|
|
1094
|
+
state: conn.connectionState
|
|
1095
|
+
},
|
|
1096
|
+
timestamp: Date.now(),
|
|
1097
|
+
id: nanoid()
|
|
1098
|
+
});
|
|
841
1099
|
}
|
|
842
1100
|
/**
|
|
843
1101
|
* Configure OAuth callback handling
|
|
@@ -881,7 +1139,7 @@ var MCPClientManager = class {
|
|
|
881
1139
|
*/
|
|
882
1140
|
getAITools() {
|
|
883
1141
|
if (!this.jsonSchema) throw new Error("jsonSchema not initialized.");
|
|
884
|
-
for (const [id, conn] of Object.entries(this.mcpConnections)) if (conn.connectionState !==
|
|
1142
|
+
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.`);
|
|
885
1143
|
return Object.fromEntries(getNamespacedData(this.mcpConnections, "tools").map((tool) => {
|
|
886
1144
|
return [`tool_${tool.serverId.replace(/-/g, "")}_${tool.name}`, {
|
|
887
1145
|
description: tool.description,
|
|
@@ -911,10 +1169,18 @@ var MCPClientManager = class {
|
|
|
911
1169
|
return this.getAITools();
|
|
912
1170
|
}
|
|
913
1171
|
/**
|
|
914
|
-
* Closes all connections to MCP servers
|
|
1172
|
+
* Closes all active in-memory connections to MCP servers.
|
|
1173
|
+
*
|
|
1174
|
+
* Note: This only closes the transport connections - it does NOT remove
|
|
1175
|
+
* servers from storage. Servers will still be listed and their callback
|
|
1176
|
+
* URLs will still match incoming OAuth requests.
|
|
1177
|
+
*
|
|
1178
|
+
* Use removeServer() instead if you want to fully clean up a server
|
|
1179
|
+
* (closes connection AND removes from storage).
|
|
915
1180
|
*/
|
|
916
1181
|
async closeAllConnections() {
|
|
917
1182
|
const ids = Object.keys(this.mcpConnections);
|
|
1183
|
+
for (const id of ids) this.mcpConnections[id].cancelDiscovery();
|
|
918
1184
|
await Promise.all(ids.map(async (id) => {
|
|
919
1185
|
await this.mcpConnections[id].client.close();
|
|
920
1186
|
}));
|
|
@@ -931,6 +1197,7 @@ var MCPClientManager = class {
|
|
|
931
1197
|
*/
|
|
932
1198
|
async closeConnection(id) {
|
|
933
1199
|
if (!this.mcpConnections[id]) throw new Error(`Connection with id "${id}" does not exist.`);
|
|
1200
|
+
this.mcpConnections[id].cancelDiscovery();
|
|
934
1201
|
await this.mcpConnections[id].client.close();
|
|
935
1202
|
delete this.mcpConnections[id];
|
|
936
1203
|
const store = this._connectionDisposables.get(id);
|
|
@@ -938,17 +1205,20 @@ var MCPClientManager = class {
|
|
|
938
1205
|
this._connectionDisposables.delete(id);
|
|
939
1206
|
}
|
|
940
1207
|
/**
|
|
941
|
-
* Remove an MCP server from storage
|
|
1208
|
+
* Remove an MCP server - closes connection if active and removes from storage.
|
|
942
1209
|
*/
|
|
943
1210
|
async removeServer(serverId) {
|
|
944
|
-
|
|
1211
|
+
if (this.mcpConnections[serverId]) try {
|
|
1212
|
+
await this.closeConnection(serverId);
|
|
1213
|
+
} catch (_e) {}
|
|
1214
|
+
this.removeServerFromStorage(serverId);
|
|
945
1215
|
this._onServerStateChanged.fire();
|
|
946
1216
|
}
|
|
947
1217
|
/**
|
|
948
1218
|
* List all MCP servers from storage
|
|
949
1219
|
*/
|
|
950
|
-
|
|
951
|
-
return
|
|
1220
|
+
listServers() {
|
|
1221
|
+
return this.getServersFromStorage();
|
|
952
1222
|
}
|
|
953
1223
|
/**
|
|
954
1224
|
* Dispose the manager and all resources.
|
|
@@ -1019,72 +1289,7 @@ function getNamespacedData(mcpClients, type) {
|
|
|
1019
1289
|
}
|
|
1020
1290
|
|
|
1021
1291
|
//#endregion
|
|
1022
|
-
//#region ../agents/dist/src-
|
|
1023
|
-
/**
|
|
1024
|
-
* SQL-based storage adapter that wraps SQL operations
|
|
1025
|
-
* Used by Agent class to provide SQL access to MCPClientManager
|
|
1026
|
-
*/
|
|
1027
|
-
var AgentMCPClientStorage = class {
|
|
1028
|
-
constructor(sql, kv) {
|
|
1029
|
-
this.sql = sql;
|
|
1030
|
-
this.kv = kv;
|
|
1031
|
-
}
|
|
1032
|
-
async saveServer(server) {
|
|
1033
|
-
this.sql`
|
|
1034
|
-
INSERT OR REPLACE INTO cf_agents_mcp_servers (
|
|
1035
|
-
id,
|
|
1036
|
-
name,
|
|
1037
|
-
server_url,
|
|
1038
|
-
client_id,
|
|
1039
|
-
auth_url,
|
|
1040
|
-
callback_url,
|
|
1041
|
-
server_options
|
|
1042
|
-
)
|
|
1043
|
-
VALUES (
|
|
1044
|
-
${server.id},
|
|
1045
|
-
${server.name},
|
|
1046
|
-
${server.server_url},
|
|
1047
|
-
${server.client_id ?? null},
|
|
1048
|
-
${server.auth_url ?? null},
|
|
1049
|
-
${server.callback_url},
|
|
1050
|
-
${server.server_options ?? null}
|
|
1051
|
-
)
|
|
1052
|
-
`;
|
|
1053
|
-
}
|
|
1054
|
-
async removeServer(serverId) {
|
|
1055
|
-
this.sql`
|
|
1056
|
-
DELETE FROM cf_agents_mcp_servers WHERE id = ${serverId}
|
|
1057
|
-
`;
|
|
1058
|
-
}
|
|
1059
|
-
async listServers() {
|
|
1060
|
-
return this.sql`
|
|
1061
|
-
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options
|
|
1062
|
-
FROM cf_agents_mcp_servers
|
|
1063
|
-
`;
|
|
1064
|
-
}
|
|
1065
|
-
async getServerByCallbackUrl(callbackUrl) {
|
|
1066
|
-
const results = this.sql`
|
|
1067
|
-
SELECT id, name, server_url, client_id, auth_url, callback_url, server_options
|
|
1068
|
-
FROM cf_agents_mcp_servers
|
|
1069
|
-
WHERE callback_url = ${callbackUrl}
|
|
1070
|
-
LIMIT 1
|
|
1071
|
-
`;
|
|
1072
|
-
return results.length > 0 ? results[0] : null;
|
|
1073
|
-
}
|
|
1074
|
-
async clearAuthUrl(serverId) {
|
|
1075
|
-
this.sql`
|
|
1076
|
-
UPDATE cf_agents_mcp_servers
|
|
1077
|
-
SET auth_url = NULL
|
|
1078
|
-
WHERE id = ${serverId}
|
|
1079
|
-
`;
|
|
1080
|
-
}
|
|
1081
|
-
async get(key) {
|
|
1082
|
-
return this.kv.get(key);
|
|
1083
|
-
}
|
|
1084
|
-
async put(key, value) {
|
|
1085
|
-
return this.kv.put(key, value);
|
|
1086
|
-
}
|
|
1087
|
-
};
|
|
1292
|
+
//#region ../agents/dist/src-BmbDclOA.js
|
|
1088
1293
|
/**
|
|
1089
1294
|
* A generic observability implementation that logs events to the console.
|
|
1090
1295
|
*/
|
|
@@ -1122,7 +1327,6 @@ function getNextCronTime(cron) {
|
|
|
1122
1327
|
const STATE_ROW_ID = "cf_state_row_id";
|
|
1123
1328
|
const STATE_WAS_CHANGED = "cf_state_was_changed";
|
|
1124
1329
|
const DEFAULT_STATE = {};
|
|
1125
|
-
const agentContext = new AsyncLocalStorage();
|
|
1126
1330
|
function getCurrentAgent() {
|
|
1127
1331
|
const store = agentContext.getStore();
|
|
1128
1332
|
if (!store) return {
|
|
@@ -1258,17 +1462,10 @@ var Agent = class Agent$1 extends Server {
|
|
|
1258
1462
|
if (this._destroyed) return;
|
|
1259
1463
|
await this._scheduleNextAlarm();
|
|
1260
1464
|
};
|
|
1261
|
-
this.mcp = new MCPClientManager(this._ParentClass.name, "0.0.1", { storage: new AgentMCPClientStorage(this.sql.bind(this), this.ctx.storage.kv) });
|
|
1262
1465
|
if (!wrappedClasses.has(this.constructor)) {
|
|
1263
1466
|
this._autoWrapCustomMethods();
|
|
1264
1467
|
wrappedClasses.add(this.constructor);
|
|
1265
1468
|
}
|
|
1266
|
-
this._disposables.add(this.mcp.onServerStateChanged(async () => {
|
|
1267
|
-
await this.broadcastMcpServers();
|
|
1268
|
-
}));
|
|
1269
|
-
this._disposables.add(this.mcp.onObservabilityEvent((event) => {
|
|
1270
|
-
this.observability?.emit(event);
|
|
1271
|
-
}));
|
|
1272
1469
|
this.sql`
|
|
1273
1470
|
CREATE TABLE IF NOT EXISTS cf_agents_mcp_servers (
|
|
1274
1471
|
id TEXT PRIMARY KEY NOT NULL,
|
|
@@ -1306,6 +1503,13 @@ var Agent = class Agent$1 extends Server {
|
|
|
1306
1503
|
created_at INTEGER DEFAULT (unixepoch())
|
|
1307
1504
|
)
|
|
1308
1505
|
`;
|
|
1506
|
+
this.mcp = new MCPClientManager(this._ParentClass.name, "0.0.1", { storage: this.ctx.storage });
|
|
1507
|
+
this._disposables.add(this.mcp.onServerStateChanged(async () => {
|
|
1508
|
+
this.broadcastMcpServers();
|
|
1509
|
+
}));
|
|
1510
|
+
this._disposables.add(this.mcp.onObservabilityEvent((event) => {
|
|
1511
|
+
this.observability?.emit(event);
|
|
1512
|
+
}));
|
|
1309
1513
|
const _onRequest = this.onRequest.bind(this);
|
|
1310
1514
|
this.onRequest = (request) => {
|
|
1311
1515
|
return agentContext.run({
|
|
@@ -1399,7 +1603,7 @@ var Agent = class Agent$1 extends Server {
|
|
|
1399
1603
|
type: MessageType.CF_AGENT_STATE
|
|
1400
1604
|
}));
|
|
1401
1605
|
connection.send(JSON.stringify({
|
|
1402
|
-
mcp:
|
|
1606
|
+
mcp: this.getMcpServers(),
|
|
1403
1607
|
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
1404
1608
|
}));
|
|
1405
1609
|
this.observability?.emit({
|
|
@@ -1422,7 +1626,7 @@ var Agent = class Agent$1 extends Server {
|
|
|
1422
1626
|
}, async () => {
|
|
1423
1627
|
await this._tryCatch(async () => {
|
|
1424
1628
|
await this.mcp.restoreConnectionsFromStorage(this.name);
|
|
1425
|
-
|
|
1629
|
+
this.broadcastMcpServers();
|
|
1426
1630
|
return _onStart(props);
|
|
1427
1631
|
});
|
|
1428
1632
|
});
|
|
@@ -1767,10 +1971,7 @@ var Agent = class Agent$1 extends Server {
|
|
|
1767
1971
|
const result = this.sql`
|
|
1768
1972
|
SELECT * FROM cf_agents_schedules WHERE id = ${id}
|
|
1769
1973
|
`;
|
|
1770
|
-
if (!result)
|
|
1771
|
-
console.error(`schedule ${id} not found`);
|
|
1772
|
-
return;
|
|
1773
|
-
}
|
|
1974
|
+
if (!result || result.length === 0) return;
|
|
1774
1975
|
return {
|
|
1775
1976
|
...result[0],
|
|
1776
1977
|
payload: JSON.parse(result[0].payload)
|
|
@@ -1807,11 +2008,12 @@ var Agent = class Agent$1 extends Server {
|
|
|
1807
2008
|
/**
|
|
1808
2009
|
* Cancel a scheduled task
|
|
1809
2010
|
* @param id ID of the task to cancel
|
|
1810
|
-
* @returns true if the task was cancelled, false
|
|
2011
|
+
* @returns true if the task was cancelled, false if the task was not found
|
|
1811
2012
|
*/
|
|
1812
2013
|
async cancelSchedule(id) {
|
|
1813
2014
|
const schedule = await this.getSchedule(id);
|
|
1814
|
-
if (schedule)
|
|
2015
|
+
if (!schedule) return false;
|
|
2016
|
+
this.observability?.emit({
|
|
1815
2017
|
displayMessage: `Schedule ${id} cancelled`,
|
|
1816
2018
|
id: nanoid(),
|
|
1817
2019
|
payload: {
|
|
@@ -1877,7 +2079,8 @@ var Agent = class Agent$1 extends Server {
|
|
|
1877
2079
|
* @param callbackHost Base host for the agent, used for the redirect URI. If not provided, will be derived from the current request.
|
|
1878
2080
|
* @param agentsPrefix agents routing prefix if not using `agents`
|
|
1879
2081
|
* @param options MCP client and transport options
|
|
1880
|
-
* @returns authUrl
|
|
2082
|
+
* @returns Server id and state - either "authenticating" with authUrl, or "ready"
|
|
2083
|
+
* @throws If connection or discovery fails
|
|
1881
2084
|
*/
|
|
1882
2085
|
async addMcpServer(serverName, url, callbackHost, agentsPrefix = "agents", options) {
|
|
1883
2086
|
let resolvedCallbackHost = callbackHost;
|
|
@@ -1890,7 +2093,7 @@ var Agent = class Agent$1 extends Server {
|
|
|
1890
2093
|
const callbackUrl = `${resolvedCallbackHost}/${agentsPrefix}/${camelCaseToKebabCase(this._ParentClass.name)}/${this.name}/callback`;
|
|
1891
2094
|
await this.mcp.ensureJsonSchema();
|
|
1892
2095
|
const id = nanoid(8);
|
|
1893
|
-
const authProvider = new DurableObjectOAuthClientProvider(this.ctx.storage
|
|
2096
|
+
const authProvider = new DurableObjectOAuthClientProvider(this.ctx.storage, this.name, callbackUrl);
|
|
1894
2097
|
authProvider.serverId = id;
|
|
1895
2098
|
const transportType = options?.transport?.type ?? "auto";
|
|
1896
2099
|
let headerTransportOpts = {};
|
|
@@ -1913,23 +2116,30 @@ var Agent = class Agent$1 extends Server {
|
|
|
1913
2116
|
}
|
|
1914
2117
|
});
|
|
1915
2118
|
const result = await this.mcp.connectToServer(id);
|
|
2119
|
+
if (result.state === MCPConnectionState.FAILED) throw new Error(`Failed to connect to MCP server at ${url}: ${result.error}`);
|
|
2120
|
+
if (result.state === MCPConnectionState.AUTHENTICATING) return {
|
|
2121
|
+
id,
|
|
2122
|
+
state: result.state,
|
|
2123
|
+
authUrl: result.authUrl
|
|
2124
|
+
};
|
|
2125
|
+
const discoverResult = await this.mcp.discoverIfConnected(id);
|
|
2126
|
+
if (discoverResult && !discoverResult.success) throw new Error(`Failed to discover MCP server capabilities: ${discoverResult.error}`);
|
|
1916
2127
|
return {
|
|
1917
2128
|
id,
|
|
1918
|
-
|
|
2129
|
+
state: MCPConnectionState.READY
|
|
1919
2130
|
};
|
|
1920
2131
|
}
|
|
1921
2132
|
async removeMcpServer(id) {
|
|
1922
|
-
if (this.mcp.mcpConnections[id]) await this.mcp.closeConnection(id);
|
|
1923
2133
|
await this.mcp.removeServer(id);
|
|
1924
2134
|
}
|
|
1925
|
-
|
|
2135
|
+
getMcpServers() {
|
|
1926
2136
|
const mcpState = {
|
|
1927
2137
|
prompts: this.mcp.listPrompts(),
|
|
1928
2138
|
resources: this.mcp.listResources(),
|
|
1929
2139
|
servers: {},
|
|
1930
2140
|
tools: this.mcp.listTools()
|
|
1931
2141
|
};
|
|
1932
|
-
const servers =
|
|
2142
|
+
const servers = this.mcp.listServers();
|
|
1933
2143
|
if (servers && Array.isArray(servers) && servers.length > 0) for (const server of servers) {
|
|
1934
2144
|
const serverConn = this.mcp.mcpConnections[server.id];
|
|
1935
2145
|
let defaultState = "not-connected";
|
|
@@ -1945,9 +2155,9 @@ var Agent = class Agent$1 extends Server {
|
|
|
1945
2155
|
}
|
|
1946
2156
|
return mcpState;
|
|
1947
2157
|
}
|
|
1948
|
-
|
|
2158
|
+
broadcastMcpServers() {
|
|
1949
2159
|
this.broadcast(JSON.stringify({
|
|
1950
|
-
mcp:
|
|
2160
|
+
mcp: this.getMcpServers(),
|
|
1951
2161
|
type: MessageType.CF_AGENT_MCP_SERVERS
|
|
1952
2162
|
}));
|
|
1953
2163
|
}
|
|
@@ -1965,16 +2175,12 @@ var Agent = class Agent$1 extends Server {
|
|
|
1965
2175
|
* @returns Response if this was an OAuth callback, null otherwise
|
|
1966
2176
|
*/
|
|
1967
2177
|
async handleMcpOAuthCallback(request) {
|
|
1968
|
-
if (!
|
|
2178
|
+
if (!this.mcp.isCallbackRequest(request)) return null;
|
|
1969
2179
|
const result = await this.mcp.handleCallbackRequest(request);
|
|
1970
|
-
if (result.authSuccess) {
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
}).finally(() => {
|
|
1975
|
-
this.broadcastMcpServers();
|
|
1976
|
-
});
|
|
1977
|
-
}
|
|
2180
|
+
if (result.authSuccess) this.mcp.establishConnection(result.serverId).catch((error) => {
|
|
2181
|
+
console.error("[Agent handleMcpOAuthCallback] Connection establishment failed:", error);
|
|
2182
|
+
});
|
|
2183
|
+
this.broadcastMcpServers();
|
|
1978
2184
|
return this.handleOAuthCallbackResponse(result, request);
|
|
1979
2185
|
}
|
|
1980
2186
|
/**
|