dynmcp 0.4.1 → 0.6.0
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.cjs +1905 -387
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1886 -368
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -23,13 +23,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
));
|
|
24
24
|
|
|
25
25
|
// src/cli.ts
|
|
26
|
-
var
|
|
26
|
+
var import_node_process14 = __toESM(require("process"), 1);
|
|
27
27
|
var import_commander = require("commander");
|
|
28
28
|
|
|
29
29
|
// package.json
|
|
30
30
|
var package_default = {
|
|
31
31
|
name: "dynmcp",
|
|
32
|
-
version: "0.
|
|
32
|
+
version: "0.6.0",
|
|
33
33
|
description: "Dynamic MCP context management tool for AI MCP-enabled agents and clients.",
|
|
34
34
|
author: "Brandon Burrus <brandon@burrus.io>",
|
|
35
35
|
license: "MIT",
|
|
@@ -91,6 +91,7 @@ var package_default = {
|
|
|
91
91
|
},
|
|
92
92
|
dependencies: {
|
|
93
93
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
94
|
+
"@napi-rs/keyring": "^1.3.0",
|
|
94
95
|
boxen: "^8.0.1",
|
|
95
96
|
chalk: "^5.6.2",
|
|
96
97
|
commander: "^14.0.3",
|
|
@@ -120,9 +121,330 @@ var package_default = {
|
|
|
120
121
|
var import_figlet = __toESM(require("figlet"), 1);
|
|
121
122
|
var import_chalk = __toESM(require("chalk"), 1);
|
|
122
123
|
|
|
123
|
-
// src/
|
|
124
|
-
var
|
|
125
|
-
|
|
124
|
+
// src/auth/errors.ts
|
|
125
|
+
var AuthRequiredError = class extends Error {
|
|
126
|
+
mcpName;
|
|
127
|
+
constructor(mcpName2) {
|
|
128
|
+
super(
|
|
129
|
+
`Upstream MCP "${mcpName2}" requires authorization. Run \`dynmcp login ${mcpName2}\` from your terminal, then retry.`
|
|
130
|
+
);
|
|
131
|
+
this.name = "AuthRequiredError";
|
|
132
|
+
this.mcpName = mcpName2;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
function isAuthRequiredError(error) {
|
|
136
|
+
let current = error;
|
|
137
|
+
for (let depth = 0; depth < 8 && current !== null && current !== void 0; depth += 1) {
|
|
138
|
+
if (current instanceof AuthRequiredError) return true;
|
|
139
|
+
if (current instanceof Error && current.name === "AuthRequiredError") return true;
|
|
140
|
+
if (current instanceof Error) {
|
|
141
|
+
current = current.cause;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/auth/keychain-store.ts
|
|
150
|
+
var import_keyring = require("@napi-rs/keyring");
|
|
151
|
+
|
|
152
|
+
// src/auth/types.ts
|
|
153
|
+
var KEYCHAIN_BLOB_VERSION = 1;
|
|
154
|
+
|
|
155
|
+
// src/auth/keychain-store.ts
|
|
156
|
+
var KEYCHAIN_SERVICE = "dynmcp";
|
|
157
|
+
function buildKeychainAccount(mcpName2, serverUrl) {
|
|
158
|
+
const origin = new URL(serverUrl).origin;
|
|
159
|
+
return `${mcpName2}:${origin}`;
|
|
160
|
+
}
|
|
161
|
+
var KeychainStore = class {
|
|
162
|
+
constructor(mcpName2, serverUrl, service = KEYCHAIN_SERVICE) {
|
|
163
|
+
this.mcpName = mcpName2;
|
|
164
|
+
this.serverUrl = serverUrl;
|
|
165
|
+
this.entry = new import_keyring.Entry(service, buildKeychainAccount(mcpName2, serverUrl));
|
|
166
|
+
}
|
|
167
|
+
mcpName;
|
|
168
|
+
serverUrl;
|
|
169
|
+
entry;
|
|
170
|
+
/**
|
|
171
|
+
* Returns the parsed blob or `undefined` if no entry exists. Entries written under
|
|
172
|
+
* a different {@link KeychainBlob.version} are treated as absent — the caller must
|
|
173
|
+
* re-authenticate. Malformed JSON also returns `undefined` (corrupt entries are not
|
|
174
|
+
* surfaced as errors; recovery is the same: re-auth).
|
|
175
|
+
*/
|
|
176
|
+
get() {
|
|
177
|
+
const raw = this.entry.getPassword();
|
|
178
|
+
if (raw === null) return void 0;
|
|
179
|
+
let parsed;
|
|
180
|
+
try {
|
|
181
|
+
parsed = JSON.parse(raw);
|
|
182
|
+
} catch {
|
|
183
|
+
return void 0;
|
|
184
|
+
}
|
|
185
|
+
if (!isCurrentVersionBlob(parsed)) {
|
|
186
|
+
return void 0;
|
|
187
|
+
}
|
|
188
|
+
return parsed;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Persists the blob atomically. Caller must construct a complete {@link
|
|
192
|
+
* KeychainBlob} — partial updates are not supported. To mutate, call {@link get},
|
|
193
|
+
* spread, and pass the result back to {@link set}.
|
|
194
|
+
*/
|
|
195
|
+
set(blob) {
|
|
196
|
+
const stamped = { ...blob, version: KEYCHAIN_BLOB_VERSION };
|
|
197
|
+
this.entry.setPassword(JSON.stringify(stamped));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Deletes the entry. Returns `true` if an entry was present and removed, `false`
|
|
201
|
+
* if there was nothing to delete. Idempotent: callers should treat both outcomes
|
|
202
|
+
* as success (a no-op delete is not an error).
|
|
203
|
+
*/
|
|
204
|
+
delete() {
|
|
205
|
+
return this.entry.deletePassword();
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
function isCurrentVersionBlob(value) {
|
|
209
|
+
return typeof value === "object" && value !== null && "version" in value && value.version === KEYCHAIN_BLOB_VERSION;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/auth/oauth-provider.ts
|
|
213
|
+
var import_node_crypto = require("crypto");
|
|
214
|
+
var CLIENT_NAME = "dynmcp";
|
|
215
|
+
var SOFTWARE_ID = "dynmcp";
|
|
216
|
+
var REFRESH_SLACK_SECONDS = 30;
|
|
217
|
+
var BaseOAuthProvider = class {
|
|
218
|
+
constructor(mcpName2, keychain, configAuth) {
|
|
219
|
+
this.mcpName = mcpName2;
|
|
220
|
+
this.keychain = keychain;
|
|
221
|
+
this.configAuth = configAuth;
|
|
222
|
+
}
|
|
223
|
+
mcpName;
|
|
224
|
+
keychain;
|
|
225
|
+
configAuth;
|
|
226
|
+
clientInformation() {
|
|
227
|
+
if (this.configAuth !== void 0) {
|
|
228
|
+
const info2 = { client_id: this.configAuth.client_id };
|
|
229
|
+
if (this.configAuth.client_secret !== void 0) {
|
|
230
|
+
info2.client_secret = this.configAuth.client_secret;
|
|
231
|
+
}
|
|
232
|
+
return info2;
|
|
233
|
+
}
|
|
234
|
+
const blob = this.keychain.get();
|
|
235
|
+
if (blob?.dcr === void 0) return void 0;
|
|
236
|
+
const info = { client_id: blob.dcr.client_id };
|
|
237
|
+
if (blob.dcr.client_secret !== void 0) {
|
|
238
|
+
info.client_secret = blob.dcr.client_secret;
|
|
239
|
+
}
|
|
240
|
+
return info;
|
|
241
|
+
}
|
|
242
|
+
tokens() {
|
|
243
|
+
const blob = this.keychain.get();
|
|
244
|
+
if (blob === void 0) return void 0;
|
|
245
|
+
const remaining = Math.max(
|
|
246
|
+
0,
|
|
247
|
+
blob.expires_at - Math.floor(Date.now() / 1e3) - REFRESH_SLACK_SECONDS
|
|
248
|
+
);
|
|
249
|
+
const tokens = {
|
|
250
|
+
access_token: blob.access_token,
|
|
251
|
+
token_type: blob.token_type,
|
|
252
|
+
expires_in: remaining
|
|
253
|
+
};
|
|
254
|
+
if (blob.refresh_token !== void 0) tokens.refresh_token = blob.refresh_token;
|
|
255
|
+
if (blob.scope_granted !== void 0) tokens.scope = blob.scope_granted;
|
|
256
|
+
return tokens;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Builds the {@link OAuthDiscoveryState} the SDK can use to skip rediscovery,
|
|
260
|
+
* reconstructed from the cached keychain blob. Returns `undefined` when there is
|
|
261
|
+
* no cached blob (e.g. fresh login flow before saveTokens fires).
|
|
262
|
+
*/
|
|
263
|
+
buildDiscoveryStateFromBlob(blob) {
|
|
264
|
+
if (blob === void 0) return void 0;
|
|
265
|
+
return {
|
|
266
|
+
authorizationServerUrl: blob.authorization_server.issuer,
|
|
267
|
+
authorizationServerMetadata: {
|
|
268
|
+
issuer: blob.authorization_server.issuer,
|
|
269
|
+
authorization_endpoint: blob.authorization_server.authorization_endpoint,
|
|
270
|
+
token_endpoint: blob.authorization_server.token_endpoint,
|
|
271
|
+
...blob.authorization_server.registration_endpoint !== void 0 ? { registration_endpoint: blob.authorization_server.registration_endpoint } : {},
|
|
272
|
+
response_types_supported: ["code"]
|
|
273
|
+
},
|
|
274
|
+
resourceMetadata: {
|
|
275
|
+
resource: blob.resource_metadata.resource,
|
|
276
|
+
authorization_servers: blob.resource_metadata.authorization_servers
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
var ProxyOAuthProvider = class extends BaseOAuthProvider {
|
|
282
|
+
get redirectUrl() {
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
285
|
+
get clientMetadata() {
|
|
286
|
+
return {
|
|
287
|
+
client_name: CLIENT_NAME,
|
|
288
|
+
software_id: SOFTWARE_ID,
|
|
289
|
+
redirect_uris: [],
|
|
290
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
291
|
+
response_types: ["code"],
|
|
292
|
+
token_endpoint_auth_method: this.configAuth?.client_secret !== void 0 ? "client_secret_basic" : "none",
|
|
293
|
+
...this.configAuth?.scope !== void 0 ? { scope: this.configAuth.scope } : {}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
saveTokens(tokens) {
|
|
297
|
+
const existing = this.keychain.get();
|
|
298
|
+
if (existing === void 0) {
|
|
299
|
+
throw new AuthRequiredError(this.mcpName);
|
|
300
|
+
}
|
|
301
|
+
const expiresAt = tokens.expires_in !== void 0 ? Math.floor(Date.now() / 1e3) + tokens.expires_in : existing.expires_at;
|
|
302
|
+
const updated = {
|
|
303
|
+
...existing,
|
|
304
|
+
access_token: tokens.access_token,
|
|
305
|
+
token_type: tokens.token_type ?? "Bearer",
|
|
306
|
+
expires_at: expiresAt,
|
|
307
|
+
refresh_token: tokens.refresh_token ?? existing.refresh_token,
|
|
308
|
+
...tokens.scope !== void 0 ? { scope_granted: tokens.scope } : {}
|
|
309
|
+
};
|
|
310
|
+
this.keychain.set(updated);
|
|
311
|
+
}
|
|
312
|
+
redirectToAuthorization(_url) {
|
|
313
|
+
throw new AuthRequiredError(this.mcpName);
|
|
314
|
+
}
|
|
315
|
+
saveCodeVerifier(_verifier) {
|
|
316
|
+
throw new AuthRequiredError(this.mcpName);
|
|
317
|
+
}
|
|
318
|
+
codeVerifier() {
|
|
319
|
+
throw new AuthRequiredError(this.mcpName);
|
|
320
|
+
}
|
|
321
|
+
discoveryState() {
|
|
322
|
+
return this.buildDiscoveryStateFromBlob(this.keychain.get());
|
|
323
|
+
}
|
|
324
|
+
invalidateCredentials(_scope) {
|
|
325
|
+
this.keychain.delete();
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
var LoginOAuthProvider = class extends BaseOAuthProvider {
|
|
329
|
+
redirectUriString;
|
|
330
|
+
pending = {};
|
|
331
|
+
callbacks;
|
|
332
|
+
constructor(opts) {
|
|
333
|
+
super(opts.mcpName, opts.keychain, opts.configAuth);
|
|
334
|
+
this.redirectUriString = opts.redirectUri;
|
|
335
|
+
this.callbacks = opts.callbacks;
|
|
336
|
+
}
|
|
337
|
+
get redirectUrl() {
|
|
338
|
+
return this.redirectUriString;
|
|
339
|
+
}
|
|
340
|
+
get clientMetadata() {
|
|
341
|
+
return {
|
|
342
|
+
client_name: CLIENT_NAME,
|
|
343
|
+
software_id: SOFTWARE_ID,
|
|
344
|
+
redirect_uris: [this.redirectUriString],
|
|
345
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
346
|
+
response_types: ["code"],
|
|
347
|
+
token_endpoint_auth_method: this.configAuth?.client_secret !== void 0 ? "client_secret_basic" : "none",
|
|
348
|
+
...this.configAuth?.scope !== void 0 ? { scope: this.configAuth.scope } : {}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
state() {
|
|
352
|
+
if (this.pending.state !== void 0) return this.pending.state;
|
|
353
|
+
const generated = (0, import_node_crypto.randomBytes)(32).toString("base64url");
|
|
354
|
+
this.pending.state = generated;
|
|
355
|
+
return generated;
|
|
356
|
+
}
|
|
357
|
+
/** The state value generated for this flow, for the callback handler to verify. */
|
|
358
|
+
get currentState() {
|
|
359
|
+
return this.pending.state;
|
|
360
|
+
}
|
|
361
|
+
clientInformation() {
|
|
362
|
+
if (this.pending.dcr !== void 0) {
|
|
363
|
+
const info = { client_id: this.pending.dcr.client_id };
|
|
364
|
+
if (this.pending.dcr.client_secret !== void 0) {
|
|
365
|
+
info.client_secret = this.pending.dcr.client_secret;
|
|
366
|
+
}
|
|
367
|
+
return info;
|
|
368
|
+
}
|
|
369
|
+
return super.clientInformation();
|
|
370
|
+
}
|
|
371
|
+
saveClientInformation(info) {
|
|
372
|
+
this.pending.dcr = info;
|
|
373
|
+
}
|
|
374
|
+
saveTokens(tokens) {
|
|
375
|
+
const discovery = this.pending.discovery ?? this.buildDiscoveryStateFromBlob(this.keychain.get());
|
|
376
|
+
if (discovery === void 0) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`Cannot persist tokens for "${this.mcpName}": no discovery state captured during the flow.`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
if (discovery.authorizationServerMetadata === void 0) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`Cannot persist tokens for "${this.mcpName}": authorization server metadata not available.`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (discovery.resourceMetadata === void 0) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`Cannot persist tokens for "${this.mcpName}": protected resource metadata not available.`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const expiresAt = tokens.expires_in !== void 0 ? Math.floor(Date.now() / 1e3) + tokens.expires_in : Math.floor(Date.now() / 1e3) + 3600;
|
|
392
|
+
const authorizationServer = {
|
|
393
|
+
issuer: discovery.authorizationServerMetadata.issuer ?? discovery.authorizationServerUrl,
|
|
394
|
+
authorization_endpoint: discovery.authorizationServerMetadata.authorization_endpoint,
|
|
395
|
+
token_endpoint: discovery.authorizationServerMetadata.token_endpoint,
|
|
396
|
+
...discovery.authorizationServerMetadata.registration_endpoint !== void 0 ? { registration_endpoint: discovery.authorizationServerMetadata.registration_endpoint } : {}
|
|
397
|
+
};
|
|
398
|
+
const resourceMetadata = {
|
|
399
|
+
resource: discovery.resourceMetadata.resource,
|
|
400
|
+
authorization_servers: discovery.resourceMetadata.authorization_servers ?? []
|
|
401
|
+
};
|
|
402
|
+
const blob = {
|
|
403
|
+
version: KEYCHAIN_BLOB_VERSION,
|
|
404
|
+
access_token: tokens.access_token,
|
|
405
|
+
token_type: tokens.token_type ?? "Bearer",
|
|
406
|
+
expires_at: expiresAt,
|
|
407
|
+
...tokens.refresh_token !== void 0 ? { refresh_token: tokens.refresh_token } : {},
|
|
408
|
+
...tokens.scope !== void 0 ? { scope_granted: tokens.scope } : {},
|
|
409
|
+
authorization_server: authorizationServer,
|
|
410
|
+
resource_metadata: resourceMetadata,
|
|
411
|
+
...this.pending.dcr !== void 0 ? {
|
|
412
|
+
dcr: {
|
|
413
|
+
client_id: this.pending.dcr.client_id,
|
|
414
|
+
...this.pending.dcr.client_secret !== void 0 ? { client_secret: this.pending.dcr.client_secret } : {}
|
|
415
|
+
}
|
|
416
|
+
} : {}
|
|
417
|
+
};
|
|
418
|
+
this.keychain.set(blob);
|
|
419
|
+
}
|
|
420
|
+
async redirectToAuthorization(url) {
|
|
421
|
+
await this.callbacks.onAuthorizationUrl(url);
|
|
422
|
+
}
|
|
423
|
+
saveCodeVerifier(verifier) {
|
|
424
|
+
this.pending.codeVerifier = verifier;
|
|
425
|
+
}
|
|
426
|
+
codeVerifier() {
|
|
427
|
+
if (this.pending.codeVerifier === void 0) {
|
|
428
|
+
throw new Error("Code verifier requested before it was saved.");
|
|
429
|
+
}
|
|
430
|
+
return this.pending.codeVerifier;
|
|
431
|
+
}
|
|
432
|
+
saveDiscoveryState(state) {
|
|
433
|
+
this.pending.discovery = state;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Force a fresh RFC 9728 + RFC 8414 discovery for every login flow. We intentionally
|
|
437
|
+
* do NOT pre-seed from the keychain on login — if endpoints changed since the last
|
|
438
|
+
* login, we want to pick them up now and persist the new snapshot.
|
|
439
|
+
*/
|
|
440
|
+
discoveryState() {
|
|
441
|
+
return void 0;
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// src/auth/login.ts
|
|
446
|
+
var import_node_process4 = __toESM(require("process"), 1);
|
|
447
|
+
var import_auth = require("@modelcontextprotocol/sdk/client/auth.js");
|
|
126
448
|
|
|
127
449
|
// src/config/schema.ts
|
|
128
450
|
var import_zod = require("zod");
|
|
@@ -144,26 +466,35 @@ var stdioTransport = import_zod.z.object({
|
|
|
144
466
|
var httpUrl = import_zod.z.string().url().refine((u) => u.startsWith("http://") || u.startsWith("https://"), {
|
|
145
467
|
message: "URL must use http:// or https:// scheme"
|
|
146
468
|
});
|
|
469
|
+
var authConfig = import_zod.z.object({
|
|
470
|
+
client_id: import_zod.z.string().min(1, { message: "auth.client_id must be a non-empty string" }).refine((value) => value.trim().length > 0, {
|
|
471
|
+
message: "auth.client_id must not be whitespace-only"
|
|
472
|
+
}),
|
|
473
|
+
client_secret: import_zod.z.string().min(1).optional(),
|
|
474
|
+
scope: import_zod.z.string().min(1).optional()
|
|
475
|
+
}).strict().optional();
|
|
147
476
|
var streamableHttpTransport = import_zod.z.object({
|
|
148
477
|
transport: import_zod.z.literal("streamable-http"),
|
|
149
478
|
description,
|
|
150
479
|
url: httpUrl,
|
|
151
|
-
headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
|
|
480
|
+
headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
|
|
481
|
+
auth: authConfig
|
|
152
482
|
}).strict();
|
|
153
483
|
var sseTransport = import_zod.z.object({
|
|
154
484
|
transport: import_zod.z.literal("sse"),
|
|
155
485
|
description,
|
|
156
486
|
url: httpUrl,
|
|
157
|
-
headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
|
|
487
|
+
headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
|
|
488
|
+
auth: authConfig
|
|
158
489
|
}).strict();
|
|
159
|
-
var
|
|
490
|
+
var transportConfigSchema = import_zod.z.discriminatedUnion("transport", [
|
|
160
491
|
stdioTransport,
|
|
161
492
|
streamableHttpTransport,
|
|
162
493
|
sseTransport
|
|
163
494
|
]);
|
|
164
495
|
var mcpConfigSchema = import_zod.z.object({
|
|
165
496
|
env: envModeSchema.optional(),
|
|
166
|
-
mcp: import_zod.z.record(mcpName,
|
|
497
|
+
mcp: import_zod.z.record(mcpName, transportConfigSchema).refine((obj) => Object.keys(obj).length > 0, { message: "At least one MCP must be configured" })
|
|
167
498
|
});
|
|
168
499
|
|
|
169
500
|
// src/config/loader.ts
|
|
@@ -230,9 +561,9 @@ function filterDefined(env) {
|
|
|
230
561
|
var TOP_LEVEL_PASSTHROUGH_KEYS = /* @__PURE__ */ new Set(["$schema", "env"]);
|
|
231
562
|
var MissingEnvVarsError = class extends Error {
|
|
232
563
|
constructor(missingVars) {
|
|
233
|
-
const
|
|
564
|
+
const list2 = missingVars.join(", ");
|
|
234
565
|
const plural = missingVars.length === 1 ? "" : "s";
|
|
235
|
-
super(`Missing required environment variable${plural}: ${
|
|
566
|
+
super(`Missing required environment variable${plural}: ${list2}`);
|
|
236
567
|
this.missingVars = missingVars;
|
|
237
568
|
this.name = "MissingEnvVarsError";
|
|
238
569
|
}
|
|
@@ -361,37 +692,1124 @@ function loadConfig(options = {}) {
|
|
|
361
692
|
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
362
693
|
throw new Error(`Failed to parse config file (${resolvedPath}): ${message}`);
|
|
363
694
|
}
|
|
364
|
-
const envMode = readEnvMode(content);
|
|
365
|
-
const loadedEnv = loadEnv({ mode: envMode, envFilePath });
|
|
366
|
-
const interpolated = loadedEnv.interpolationEnabled ? interpolateConfig(content, loadedEnv.variables) : content;
|
|
367
|
-
const result = mcpConfigSchema.safeParse(interpolated);
|
|
368
|
-
if (!result.success) {
|
|
369
|
-
const formatted = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
|
|
370
|
-
throw new Error(`Invalid config file (${resolvedPath}):
|
|
371
|
-
${formatted}`);
|
|
695
|
+
const envMode = readEnvMode(content);
|
|
696
|
+
const loadedEnv = loadEnv({ mode: envMode, envFilePath });
|
|
697
|
+
const interpolated = loadedEnv.interpolationEnabled ? interpolateConfig(content, loadedEnv.variables) : content;
|
|
698
|
+
const result = mcpConfigSchema.safeParse(interpolated);
|
|
699
|
+
if (!result.success) {
|
|
700
|
+
const formatted = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
|
|
701
|
+
throw new Error(`Invalid config file (${resolvedPath}):
|
|
702
|
+
${formatted}`);
|
|
703
|
+
}
|
|
704
|
+
return result.data;
|
|
705
|
+
}
|
|
706
|
+
function readEnvMode(content) {
|
|
707
|
+
if (content === null || typeof content !== "object" || Array.isArray(content)) {
|
|
708
|
+
return DEFAULT_ENV_MODE;
|
|
709
|
+
}
|
|
710
|
+
const value = content.env;
|
|
711
|
+
if (value === void 0) return DEFAULT_ENV_MODE;
|
|
712
|
+
if (typeof value === "string" && VALID_ENV_MODES.includes(value)) {
|
|
713
|
+
return value;
|
|
714
|
+
}
|
|
715
|
+
return DEFAULT_ENV_MODE;
|
|
716
|
+
}
|
|
717
|
+
function isYamlFile(filePath) {
|
|
718
|
+
return filePath.endsWith(".yml") || filePath.endsWith(".yaml");
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/config/json-schema.ts
|
|
722
|
+
var import_zod2 = require("zod");
|
|
723
|
+
|
|
724
|
+
// src/auth/browser.ts
|
|
725
|
+
var import_node_child_process = require("child_process");
|
|
726
|
+
var import_node_process3 = __toESM(require("process"), 1);
|
|
727
|
+
async function openUrl(url) {
|
|
728
|
+
const { command, args } = openerForPlatform(url);
|
|
729
|
+
return new Promise((resolve4, reject) => {
|
|
730
|
+
const child = (0, import_node_child_process.spawn)(command, args, {
|
|
731
|
+
stdio: "ignore",
|
|
732
|
+
detached: true
|
|
733
|
+
});
|
|
734
|
+
child.once("error", reject);
|
|
735
|
+
child.once("spawn", () => {
|
|
736
|
+
child.unref();
|
|
737
|
+
resolve4();
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
function openerForPlatform(url) {
|
|
742
|
+
switch (import_node_process3.default.platform) {
|
|
743
|
+
case "darwin":
|
|
744
|
+
return { command: "open", args: [url] };
|
|
745
|
+
case "win32":
|
|
746
|
+
return { command: "cmd", args: ["/c", "start", '""', url] };
|
|
747
|
+
default:
|
|
748
|
+
return { command: "xdg-open", args: [url] };
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// src/auth/callback-server.ts
|
|
753
|
+
var import_node_http = require("http");
|
|
754
|
+
var CallbackTimeoutError = class extends Error {
|
|
755
|
+
constructor(timeoutMs) {
|
|
756
|
+
super(`Timed out after ${timeoutMs}ms waiting for the OAuth callback.`);
|
|
757
|
+
this.name = "CallbackTimeoutError";
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
var CallbackOAuthError = class extends Error {
|
|
761
|
+
constructor(oauthError, oauthErrorDescription) {
|
|
762
|
+
super(
|
|
763
|
+
oauthErrorDescription ? `OAuth error from authorization server: ${oauthError} \u2014 ${oauthErrorDescription}` : `OAuth error from authorization server: ${oauthError}`
|
|
764
|
+
);
|
|
765
|
+
this.oauthError = oauthError;
|
|
766
|
+
this.oauthErrorDescription = oauthErrorDescription;
|
|
767
|
+
this.name = "CallbackOAuthError";
|
|
768
|
+
}
|
|
769
|
+
oauthError;
|
|
770
|
+
oauthErrorDescription;
|
|
771
|
+
};
|
|
772
|
+
var SUCCESS_HTML = `<!DOCTYPE html>
|
|
773
|
+
<html lang="en">
|
|
774
|
+
<head>
|
|
775
|
+
<meta charset="utf-8" />
|
|
776
|
+
<title>dynmcp \u2014 authorization complete</title>
|
|
777
|
+
<style>
|
|
778
|
+
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 4rem; max-width: 36rem; margin: 0 auto; color: #222; }
|
|
779
|
+
code { background: #f4f4f4; padding: 0.1rem 0.3rem; border-radius: 3px; }
|
|
780
|
+
</style>
|
|
781
|
+
</head>
|
|
782
|
+
<body>
|
|
783
|
+
<h1>Authorization complete</h1>
|
|
784
|
+
<p>You may close this tab and return to your terminal.</p>
|
|
785
|
+
<p><small>Issued by <code>dynmcp</code>.</small></p>
|
|
786
|
+
</body>
|
|
787
|
+
</html>
|
|
788
|
+
`;
|
|
789
|
+
var ERROR_HTML_PREFIX = `<!DOCTYPE html>
|
|
790
|
+
<html lang="en">
|
|
791
|
+
<head><meta charset="utf-8" /><title>dynmcp \u2014 authorization failed</title></head>
|
|
792
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 4rem; max-width: 36rem; margin: 0 auto;">
|
|
793
|
+
<h1>Authorization failed</h1>
|
|
794
|
+
<p>`;
|
|
795
|
+
var ERROR_HTML_SUFFIX = `</p>
|
|
796
|
+
<p>Return to your terminal for details.</p>
|
|
797
|
+
</body>
|
|
798
|
+
</html>
|
|
799
|
+
`;
|
|
800
|
+
var CallbackServer = class _CallbackServer {
|
|
801
|
+
server = null;
|
|
802
|
+
boundPort = null;
|
|
803
|
+
pending = null;
|
|
804
|
+
/** The redirect path served. Must match the redirect URI registered with the OAuth client. */
|
|
805
|
+
static CALLBACK_PATH = "/callback";
|
|
806
|
+
/** Begins listening on `127.0.0.1` at an OS-assigned port. */
|
|
807
|
+
async start() {
|
|
808
|
+
if (this.server !== null) {
|
|
809
|
+
throw new Error("CallbackServer is already started.");
|
|
810
|
+
}
|
|
811
|
+
const server = (0, import_node_http.createServer)((req, res) => this.handleRequest(req, res));
|
|
812
|
+
await new Promise((resolve4, reject) => {
|
|
813
|
+
const onError = (err) => {
|
|
814
|
+
server.removeListener("listening", onListening);
|
|
815
|
+
reject(err);
|
|
816
|
+
};
|
|
817
|
+
const onListening = () => {
|
|
818
|
+
server.removeListener("error", onError);
|
|
819
|
+
resolve4();
|
|
820
|
+
};
|
|
821
|
+
server.once("error", onError);
|
|
822
|
+
server.once("listening", onListening);
|
|
823
|
+
server.listen({ port: 0, host: "127.0.0.1" });
|
|
824
|
+
});
|
|
825
|
+
const address = server.address();
|
|
826
|
+
if (address === null || typeof address === "string") {
|
|
827
|
+
server.close();
|
|
828
|
+
throw new Error("Failed to determine bound port for callback server.");
|
|
829
|
+
}
|
|
830
|
+
this.boundPort = address.port;
|
|
831
|
+
this.server = server;
|
|
832
|
+
}
|
|
833
|
+
/** The port the OS assigned. Available after {@link start} resolves. */
|
|
834
|
+
get port() {
|
|
835
|
+
if (this.boundPort === null) {
|
|
836
|
+
throw new Error("CallbackServer is not started.");
|
|
837
|
+
}
|
|
838
|
+
return this.boundPort;
|
|
839
|
+
}
|
|
840
|
+
/** The full redirect URI to register with the authorization server. */
|
|
841
|
+
get redirectUri() {
|
|
842
|
+
return `http://127.0.0.1:${this.port}${_CallbackServer.CALLBACK_PATH}`;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Resolves with the captured `code` and `state` once a valid callback is received,
|
|
846
|
+
* or rejects with {@link CallbackTimeoutError} / {@link CallbackOAuthError} on the
|
|
847
|
+
* documented failure paths. Only one callback is accepted; subsequent requests to
|
|
848
|
+
* `/callback` after the first valid hit return `400`.
|
|
849
|
+
*/
|
|
850
|
+
awaitCallback(timeoutMs) {
|
|
851
|
+
if (this.server === null) {
|
|
852
|
+
return Promise.reject(new Error("CallbackServer is not started."));
|
|
853
|
+
}
|
|
854
|
+
if (this.pending !== null) {
|
|
855
|
+
return Promise.reject(new Error("awaitCallback already in progress."));
|
|
856
|
+
}
|
|
857
|
+
return new Promise((resolve4, reject) => {
|
|
858
|
+
const timer = setTimeout(() => {
|
|
859
|
+
this.pending = null;
|
|
860
|
+
reject(new CallbackTimeoutError(timeoutMs));
|
|
861
|
+
}, timeoutMs);
|
|
862
|
+
this.pending = { resolve: resolve4, reject, timer };
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
/** Closes the listening socket. Safe to call multiple times. */
|
|
866
|
+
async stop() {
|
|
867
|
+
if (this.pending !== null) {
|
|
868
|
+
clearTimeout(this.pending.timer);
|
|
869
|
+
this.pending = null;
|
|
870
|
+
}
|
|
871
|
+
if (this.server === null) return;
|
|
872
|
+
await new Promise((resolve4) => {
|
|
873
|
+
this.server.close(() => resolve4());
|
|
874
|
+
});
|
|
875
|
+
this.server = null;
|
|
876
|
+
this.boundPort = null;
|
|
877
|
+
}
|
|
878
|
+
handleRequest(req, res) {
|
|
879
|
+
if (req.method !== "GET") {
|
|
880
|
+
res.writeHead(405, { "content-type": "text/plain", allow: "GET" });
|
|
881
|
+
res.end("Method Not Allowed");
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${this.boundPort ?? 0}`);
|
|
885
|
+
if (url.pathname !== _CallbackServer.CALLBACK_PATH) {
|
|
886
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
887
|
+
res.end("Not Found");
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (this.pending === null) {
|
|
891
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
892
|
+
res.end("No callback expected.");
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const oauthError = url.searchParams.get("error");
|
|
896
|
+
if (oauthError !== null) {
|
|
897
|
+
const errorDescription = url.searchParams.get("error_description") ?? void 0;
|
|
898
|
+
this.respondError(res, oauthError, errorDescription);
|
|
899
|
+
const { reject, timer: timer2 } = this.pending;
|
|
900
|
+
clearTimeout(timer2);
|
|
901
|
+
this.pending = null;
|
|
902
|
+
reject(new CallbackOAuthError(oauthError, errorDescription));
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const code = url.searchParams.get("code");
|
|
906
|
+
const state = url.searchParams.get("state");
|
|
907
|
+
if (code === null || state === null) {
|
|
908
|
+
this.respondError(res, "invalid_callback", "Missing code or state parameter.");
|
|
909
|
+
const { reject, timer: timer2 } = this.pending;
|
|
910
|
+
clearTimeout(timer2);
|
|
911
|
+
this.pending = null;
|
|
912
|
+
reject(new Error("Callback missing code or state parameter."));
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
916
|
+
res.end(SUCCESS_HTML);
|
|
917
|
+
const { resolve: resolve4, timer } = this.pending;
|
|
918
|
+
clearTimeout(timer);
|
|
919
|
+
this.pending = null;
|
|
920
|
+
resolve4({ code, state });
|
|
921
|
+
}
|
|
922
|
+
respondError(res, errorCode, description2) {
|
|
923
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
924
|
+
res.end(
|
|
925
|
+
ERROR_HTML_PREFIX + escapeHtml(description2 ?? `OAuth error: ${errorCode}`) + ERROR_HTML_SUFFIX
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
function escapeHtml(value) {
|
|
930
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/auth/login.ts
|
|
934
|
+
var CALLBACK_TIMEOUT_MS = 6e4;
|
|
935
|
+
async function login(options) {
|
|
936
|
+
const config = loadConfig({
|
|
937
|
+
configPath: options.configPath,
|
|
938
|
+
envFilePath: options.envFilePath
|
|
939
|
+
});
|
|
940
|
+
const entry = resolveOAuthCapableEntry(config, options.mcpName);
|
|
941
|
+
const writeStatus = options.writeStatus ?? defaultStatusWriter;
|
|
942
|
+
const openInBrowser = options.openInBrowser ?? openUrl;
|
|
943
|
+
writeStatus(`Probing ${entry.url} for OAuth challenge...
|
|
944
|
+
`);
|
|
945
|
+
const resourceMetadataUrl = await probeFor401ResourceMetadata(entry.url);
|
|
946
|
+
if (resourceMetadataUrl === void 0) {
|
|
947
|
+
throw new Error(
|
|
948
|
+
`Upstream "${options.mcpName}" did not return a 401 challenge with a WWW-Authenticate \`resource_metadata\` URL. The server does not appear to require OAuth; no credentials stored.`
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
const keychain = new KeychainStore(options.mcpName, entry.url);
|
|
952
|
+
const callbackServer = new CallbackServer();
|
|
953
|
+
await callbackServer.start();
|
|
954
|
+
writeStatus(`Callback server listening on ${callbackServer.redirectUri}
|
|
955
|
+
`);
|
|
956
|
+
try {
|
|
957
|
+
const provider = new LoginOAuthProvider({
|
|
958
|
+
mcpName: options.mcpName,
|
|
959
|
+
keychain,
|
|
960
|
+
configAuth: configAuthFromEntry(entry),
|
|
961
|
+
redirectUri: callbackServer.redirectUri,
|
|
962
|
+
callbacks: {
|
|
963
|
+
onAuthorizationUrl: async (url) => {
|
|
964
|
+
writeStatus(`Opening browser for authorization: ${url.toString()}
|
|
965
|
+
`);
|
|
966
|
+
try {
|
|
967
|
+
await openInBrowser(url.toString());
|
|
968
|
+
} catch (error) {
|
|
969
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
970
|
+
writeStatus(
|
|
971
|
+
`Failed to launch browser (${reason}). Open this URL manually:
|
|
972
|
+
${url.toString()}
|
|
973
|
+
`
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
const firstResult = await (0, import_auth.auth)(provider, {
|
|
980
|
+
serverUrl: entry.url,
|
|
981
|
+
resourceMetadataUrl,
|
|
982
|
+
...entry.auth?.scope !== void 0 ? { scope: entry.auth.scope } : {}
|
|
983
|
+
});
|
|
984
|
+
if (firstResult === "AUTHORIZED") {
|
|
985
|
+
writeStatus(`Already authorized for "${options.mcpName}"; no changes made.
|
|
986
|
+
`);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
writeStatus(`Waiting for browser callback (timeout ${CALLBACK_TIMEOUT_MS / 1e3}s)...
|
|
990
|
+
`);
|
|
991
|
+
const { code, state: receivedState } = await callbackServer.awaitCallback(CALLBACK_TIMEOUT_MS);
|
|
992
|
+
const expectedState = provider.currentState;
|
|
993
|
+
if (expectedState === void 0 || receivedState !== expectedState) {
|
|
994
|
+
throw new Error(
|
|
995
|
+
"OAuth state mismatch on callback. Possible CSRF attempt or stale browser tab; not exchanging the authorization code."
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
const secondResult = await (0, import_auth.auth)(provider, {
|
|
999
|
+
serverUrl: entry.url,
|
|
1000
|
+
authorizationCode: code,
|
|
1001
|
+
resourceMetadataUrl,
|
|
1002
|
+
...entry.auth?.scope !== void 0 ? { scope: entry.auth.scope } : {}
|
|
1003
|
+
});
|
|
1004
|
+
if (secondResult !== "AUTHORIZED") {
|
|
1005
|
+
throw new Error(`Token exchange did not return AUTHORIZED (got ${secondResult}).`);
|
|
1006
|
+
}
|
|
1007
|
+
writeStatus(`Successfully authenticated "${options.mcpName}".
|
|
1008
|
+
`);
|
|
1009
|
+
} finally {
|
|
1010
|
+
await callbackServer.stop();
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function resolveOAuthCapableEntry(config, mcpName2) {
|
|
1014
|
+
const entry = config.mcp[mcpName2];
|
|
1015
|
+
if (entry === void 0) {
|
|
1016
|
+
const available = Object.keys(config.mcp).sort().join(", ");
|
|
1017
|
+
throw new Error(`Unknown MCP "${mcpName2}". Configured MCPs: ${available || "(none)"}.`);
|
|
1018
|
+
}
|
|
1019
|
+
if (entry.transport !== "streamable-http" && entry.transport !== "sse") {
|
|
1020
|
+
throw new Error(
|
|
1021
|
+
`MCP "${mcpName2}" uses the "${entry.transport}" transport; OAuth is only supported for streamable-http and sse upstreams.`
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
return entry;
|
|
1025
|
+
}
|
|
1026
|
+
function configAuthFromEntry(entry) {
|
|
1027
|
+
if (entry.auth === void 0) return void 0;
|
|
1028
|
+
const overrides = { client_id: entry.auth.client_id };
|
|
1029
|
+
if (entry.auth.client_secret !== void 0) overrides.client_secret = entry.auth.client_secret;
|
|
1030
|
+
if (entry.auth.scope !== void 0) overrides.scope = entry.auth.scope;
|
|
1031
|
+
return overrides;
|
|
1032
|
+
}
|
|
1033
|
+
async function probeFor401ResourceMetadata(serverUrl) {
|
|
1034
|
+
let response;
|
|
1035
|
+
try {
|
|
1036
|
+
response = await fetch(serverUrl, { method: "GET", redirect: "manual" });
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1039
|
+
throw new Error(`Failed to reach ${serverUrl}: ${reason}`);
|
|
1040
|
+
}
|
|
1041
|
+
if (response.status !== 401) {
|
|
1042
|
+
return void 0;
|
|
1043
|
+
}
|
|
1044
|
+
const { resourceMetadataUrl } = (0, import_auth.extractWWWAuthenticateParams)(response);
|
|
1045
|
+
return resourceMetadataUrl;
|
|
1046
|
+
}
|
|
1047
|
+
function defaultStatusWriter(message) {
|
|
1048
|
+
import_node_process4.default.stderr.write(message);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// src/auth/logout.ts
|
|
1052
|
+
var import_node_process5 = __toESM(require("process"), 1);
|
|
1053
|
+
async function logout(options) {
|
|
1054
|
+
const config = loadConfig({
|
|
1055
|
+
configPath: options.configPath,
|
|
1056
|
+
envFilePath: options.envFilePath
|
|
1057
|
+
});
|
|
1058
|
+
const entry = resolveOAuthCapableEntry2(config, options.mcpName);
|
|
1059
|
+
const writeStatus = options.writeStatus ?? defaultStatusWriter2;
|
|
1060
|
+
const keychain = new KeychainStore(options.mcpName, entry.url);
|
|
1061
|
+
const removed = keychain.delete();
|
|
1062
|
+
if (removed) {
|
|
1063
|
+
writeStatus(`Removed keychain credentials for "${options.mcpName}".
|
|
1064
|
+
`);
|
|
1065
|
+
} else {
|
|
1066
|
+
writeStatus(
|
|
1067
|
+
`No keychain credentials were stored for "${options.mcpName}"; nothing to remove.
|
|
1068
|
+
`
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function resolveOAuthCapableEntry2(config, mcpName2) {
|
|
1073
|
+
const entry = config.mcp[mcpName2];
|
|
1074
|
+
if (entry === void 0) {
|
|
1075
|
+
const available = Object.keys(config.mcp).sort().join(", ");
|
|
1076
|
+
throw new Error(`Unknown MCP "${mcpName2}". Configured MCPs: ${available || "(none)"}.`);
|
|
1077
|
+
}
|
|
1078
|
+
if (entry.transport !== "streamable-http" && entry.transport !== "sse") {
|
|
1079
|
+
throw new Error(
|
|
1080
|
+
`MCP "${mcpName2}" uses the "${entry.transport}" transport; OAuth is only supported for streamable-http and sse upstreams.`
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
return entry;
|
|
1084
|
+
}
|
|
1085
|
+
function defaultStatusWriter2(message) {
|
|
1086
|
+
import_node_process5.default.stderr.write(message);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/diagnostics/list.ts
|
|
1090
|
+
var import_node_process6 = __toESM(require("process"), 1);
|
|
1091
|
+
|
|
1092
|
+
// src/diagnostics/format.ts
|
|
1093
|
+
function renderTable(headers, rows) {
|
|
1094
|
+
const allRows = [headers, ...rows];
|
|
1095
|
+
const widths = headers.map(
|
|
1096
|
+
(_, colIdx) => Math.max(...allRows.map((row) => (row[colIdx] ?? "").length))
|
|
1097
|
+
);
|
|
1098
|
+
return allRows.map(
|
|
1099
|
+
(row) => row.map((cell, i) => {
|
|
1100
|
+
if (i === headers.length - 1) return cell;
|
|
1101
|
+
return cell.padEnd(widths[i] ?? 0);
|
|
1102
|
+
}).join(" ").trimEnd()
|
|
1103
|
+
).join("\n");
|
|
1104
|
+
}
|
|
1105
|
+
function truncate(value, max) {
|
|
1106
|
+
if (value.length <= max) return value;
|
|
1107
|
+
if (max <= 3) return value.slice(0, max);
|
|
1108
|
+
return `${value.slice(0, max - 3)}...`;
|
|
1109
|
+
}
|
|
1110
|
+
function humanizeDuration(seconds) {
|
|
1111
|
+
if (seconds < 0) return "expired";
|
|
1112
|
+
if (seconds < 60) return `${Math.floor(seconds)}s`;
|
|
1113
|
+
const totalMinutes = Math.floor(seconds / 60);
|
|
1114
|
+
if (totalMinutes < 60) return `${totalMinutes}m`;
|
|
1115
|
+
const totalHours = Math.floor(totalMinutes / 60);
|
|
1116
|
+
const remainingMinutes = totalMinutes % 60;
|
|
1117
|
+
if (totalHours < 24) {
|
|
1118
|
+
return remainingMinutes > 0 ? `${totalHours}h ${remainingMinutes}m` : `${totalHours}h`;
|
|
1119
|
+
}
|
|
1120
|
+
const totalDays = Math.floor(totalHours / 24);
|
|
1121
|
+
const remainingHours = totalHours % 24;
|
|
1122
|
+
return remainingHours > 0 ? `${totalDays}d ${remainingHours}h` : `${totalDays}d`;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// src/diagnostics/list.ts
|
|
1126
|
+
var ENDPOINT_MAX_WIDTH = 48;
|
|
1127
|
+
async function list(options = {}) {
|
|
1128
|
+
const config = loadConfig({
|
|
1129
|
+
configPath: options.configPath,
|
|
1130
|
+
envFilePath: options.envFilePath
|
|
1131
|
+
});
|
|
1132
|
+
const write = options.write ?? ((chunk) => void import_node_process6.default.stdout.write(chunk));
|
|
1133
|
+
const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
1134
|
+
const entries = buildEntries(config, now);
|
|
1135
|
+
if (options.json === true) {
|
|
1136
|
+
write(`${JSON.stringify(entries, null, 2)}
|
|
1137
|
+
`);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
if (entries.length === 0) {
|
|
1141
|
+
write("No upstream MCPs configured.\n");
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const headers = ["NAME", "TRANSPORT", "MODE", "ENDPOINT", "AUTH"];
|
|
1145
|
+
const rows = entries.map((entry) => [
|
|
1146
|
+
entry.name,
|
|
1147
|
+
entry.transport,
|
|
1148
|
+
entry.mode,
|
|
1149
|
+
truncate(entry.endpoint, ENDPOINT_MAX_WIDTH),
|
|
1150
|
+
formatAuthStatus(entry.auth)
|
|
1151
|
+
]);
|
|
1152
|
+
write(`${renderTable(headers, rows)}
|
|
1153
|
+
`);
|
|
1154
|
+
}
|
|
1155
|
+
function buildEntries(config, now) {
|
|
1156
|
+
return Object.entries(config.mcp).map(([name, entry]) => {
|
|
1157
|
+
const mode = entry.description !== void 0 ? "lazy" : "eager";
|
|
1158
|
+
if (entry.transport === "stdio") {
|
|
1159
|
+
const command = entry.command;
|
|
1160
|
+
const args = (entry.args ?? []).join(" ");
|
|
1161
|
+
const endpoint = args.length > 0 ? `${command} ${args}` : command;
|
|
1162
|
+
const built2 = {
|
|
1163
|
+
name,
|
|
1164
|
+
transport: "stdio",
|
|
1165
|
+
mode,
|
|
1166
|
+
endpoint,
|
|
1167
|
+
auth: { kind: "n/a" }
|
|
1168
|
+
};
|
|
1169
|
+
if (entry.description !== void 0) built2.description = entry.description;
|
|
1170
|
+
return built2;
|
|
1171
|
+
}
|
|
1172
|
+
const hasAuthHeader = hasBearerAuthHeader(entry.headers);
|
|
1173
|
+
const keychain = new KeychainStore(name, entry.url);
|
|
1174
|
+
const blob = keychain.get();
|
|
1175
|
+
let auth2;
|
|
1176
|
+
if (blob !== void 0) {
|
|
1177
|
+
const expiresInSeconds = blob.expires_at - now();
|
|
1178
|
+
auth2 = {
|
|
1179
|
+
kind: "oauth",
|
|
1180
|
+
status: "logged_in",
|
|
1181
|
+
expiresInSeconds,
|
|
1182
|
+
expiresAt: blob.expires_at
|
|
1183
|
+
};
|
|
1184
|
+
if (hasAuthHeader) auth2.alsoHeader = true;
|
|
1185
|
+
} else if (hasAuthHeader) {
|
|
1186
|
+
auth2 = { kind: "header" };
|
|
1187
|
+
} else {
|
|
1188
|
+
auth2 = { kind: "oauth", status: "not_logged_in" };
|
|
1189
|
+
}
|
|
1190
|
+
const built = {
|
|
1191
|
+
name,
|
|
1192
|
+
transport: entry.transport,
|
|
1193
|
+
mode,
|
|
1194
|
+
endpoint: entry.url,
|
|
1195
|
+
auth: auth2
|
|
1196
|
+
};
|
|
1197
|
+
if (entry.description !== void 0) built.description = entry.description;
|
|
1198
|
+
return built;
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
function hasBearerAuthHeader(headers) {
|
|
1202
|
+
if (headers === void 0) return false;
|
|
1203
|
+
for (const key of Object.keys(headers)) {
|
|
1204
|
+
if (key.toLowerCase() === "authorization") return true;
|
|
1205
|
+
}
|
|
1206
|
+
return false;
|
|
1207
|
+
}
|
|
1208
|
+
function formatAuthStatus(auth2) {
|
|
1209
|
+
switch (auth2.kind) {
|
|
1210
|
+
case "n/a":
|
|
1211
|
+
return "n/a";
|
|
1212
|
+
case "header":
|
|
1213
|
+
return "header";
|
|
1214
|
+
case "oauth": {
|
|
1215
|
+
if (auth2.status === "not_logged_in") return "oauth: not logged in";
|
|
1216
|
+
const duration = humanizeDuration(auth2.expiresInSeconds ?? 0);
|
|
1217
|
+
const base = `oauth: logged in (expires in ${duration})`;
|
|
1218
|
+
return auth2.alsoHeader === true ? `${base} (header also set)` : base;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/diagnostics/test.ts
|
|
1224
|
+
var import_node_process8 = __toESM(require("process"), 1);
|
|
1225
|
+
|
|
1226
|
+
// src/proxy/transport-factory.ts
|
|
1227
|
+
var import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
1228
|
+
var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
1229
|
+
var import_sse = require("@modelcontextprotocol/sdk/client/sse.js");
|
|
1230
|
+
function createTransport(mcpName2, config) {
|
|
1231
|
+
switch (config.transport) {
|
|
1232
|
+
case "stdio":
|
|
1233
|
+
return new import_stdio.StdioClientTransport({
|
|
1234
|
+
command: config.command,
|
|
1235
|
+
args: config.args,
|
|
1236
|
+
env: config.env
|
|
1237
|
+
});
|
|
1238
|
+
case "streamable-http":
|
|
1239
|
+
return new import_streamableHttp.StreamableHTTPClientTransport(new URL(config.url), {
|
|
1240
|
+
...config.headers !== void 0 ? { requestInit: { headers: config.headers } } : {},
|
|
1241
|
+
authProvider: buildOAuthProvider(mcpName2, config)
|
|
1242
|
+
});
|
|
1243
|
+
case "sse":
|
|
1244
|
+
return new import_sse.SSEClientTransport(new URL(config.url), {
|
|
1245
|
+
...config.headers !== void 0 ? { requestInit: { headers: config.headers } } : {},
|
|
1246
|
+
authProvider: buildOAuthProvider(mcpName2, config)
|
|
1247
|
+
});
|
|
1248
|
+
default: {
|
|
1249
|
+
const _exhaustive = config;
|
|
1250
|
+
return _exhaustive;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
function buildOAuthProvider(mcpName2, config) {
|
|
1255
|
+
const keychain = new KeychainStore(mcpName2, config.url);
|
|
1256
|
+
return new ProxyOAuthProvider(mcpName2, keychain, config.auth);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// src/proxy/upstream-client.ts
|
|
1260
|
+
var import_node_process7 = __toESM(require("process"), 1);
|
|
1261
|
+
var import_client = require("@modelcontextprotocol/sdk/client/index.js");
|
|
1262
|
+
var import_types4 = require("@modelcontextprotocol/sdk/types.js");
|
|
1263
|
+
var UpstreamClient = class {
|
|
1264
|
+
transport;
|
|
1265
|
+
onTransportError;
|
|
1266
|
+
notificationHandlers;
|
|
1267
|
+
serverRequestHandlers;
|
|
1268
|
+
client = null;
|
|
1269
|
+
constructor({
|
|
1270
|
+
name,
|
|
1271
|
+
transport,
|
|
1272
|
+
onTransportError,
|
|
1273
|
+
notifications,
|
|
1274
|
+
serverRequests
|
|
1275
|
+
}) {
|
|
1276
|
+
this.transport = transport;
|
|
1277
|
+
this.notificationHandlers = notifications ?? {};
|
|
1278
|
+
this.serverRequestHandlers = serverRequests ?? {};
|
|
1279
|
+
this.onTransportError = onTransportError ?? ((error) => {
|
|
1280
|
+
import_node_process7.default.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
|
|
1281
|
+
`);
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
async connect() {
|
|
1285
|
+
this.transport.onerror = this.onTransportError;
|
|
1286
|
+
this.client = new import_client.Client(
|
|
1287
|
+
{ name: "dynamic-discovery-mcp", version: "1.0.0" },
|
|
1288
|
+
{
|
|
1289
|
+
capabilities: {
|
|
1290
|
+
// Declare every client-side capability the proxy may relay on behalf of the host.
|
|
1291
|
+
// Actual reachability of each feature depends on what the host supports — if the
|
|
1292
|
+
// host does not support sampling, for instance, the host call returns an error
|
|
1293
|
+
// which we forward back to the upstream verbatim.
|
|
1294
|
+
sampling: {},
|
|
1295
|
+
elicitation: {},
|
|
1296
|
+
roots: { listChanged: true }
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
);
|
|
1300
|
+
this.registerServerRequestHandlers(this.client);
|
|
1301
|
+
if (this.notificationHandlers.onToolsListChanged !== void 0) {
|
|
1302
|
+
this.client.setNotificationHandler(import_types4.ToolListChangedNotificationSchema, async () => {
|
|
1303
|
+
await this.notificationHandlers.onToolsListChanged?.();
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
if (this.notificationHandlers.onResourcesListChanged !== void 0) {
|
|
1307
|
+
this.client.setNotificationHandler(import_types4.ResourceListChangedNotificationSchema, async () => {
|
|
1308
|
+
await this.notificationHandlers.onResourcesListChanged?.();
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
if (this.notificationHandlers.onResourceUpdated !== void 0) {
|
|
1312
|
+
this.client.setNotificationHandler(import_types4.ResourceUpdatedNotificationSchema, async (notification) => {
|
|
1313
|
+
await this.notificationHandlers.onResourceUpdated?.({ uri: notification.params.uri });
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
if (this.notificationHandlers.onPromptsListChanged !== void 0) {
|
|
1317
|
+
this.client.setNotificationHandler(import_types4.PromptListChangedNotificationSchema, async () => {
|
|
1318
|
+
await this.notificationHandlers.onPromptsListChanged?.();
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
if (this.notificationHandlers.onLogMessage !== void 0) {
|
|
1322
|
+
this.client.setNotificationHandler(import_types4.LoggingMessageNotificationSchema, async (notification) => {
|
|
1323
|
+
await this.notificationHandlers.onLogMessage?.(notification.params);
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
await this.client.connect(this.transport);
|
|
1327
|
+
}
|
|
1328
|
+
async setLoggingLevel(level, options) {
|
|
1329
|
+
const client = this.requireClient();
|
|
1330
|
+
await client.setLoggingLevel(level, options);
|
|
1331
|
+
}
|
|
1332
|
+
async listPrompts(options) {
|
|
1333
|
+
const client = this.requireClient();
|
|
1334
|
+
const result = await client.listPrompts(void 0, options);
|
|
1335
|
+
return result.prompts;
|
|
1336
|
+
}
|
|
1337
|
+
async getPrompt(name, args, options) {
|
|
1338
|
+
const client = this.requireClient();
|
|
1339
|
+
const params = { name };
|
|
1340
|
+
if (args !== void 0) {
|
|
1341
|
+
params.arguments = args;
|
|
1342
|
+
}
|
|
1343
|
+
return client.getPrompt(params, options);
|
|
1344
|
+
}
|
|
1345
|
+
async complete(params, options) {
|
|
1346
|
+
const client = this.requireClient();
|
|
1347
|
+
return client.complete(params, options);
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Returns the capabilities advertised by the upstream server during initialize.
|
|
1351
|
+
* Returns `undefined` if the client is not connected, or if the SDK has not yet
|
|
1352
|
+
* recorded the server's capabilities (e.g. during a partially-completed handshake).
|
|
1353
|
+
*/
|
|
1354
|
+
getCapabilities() {
|
|
1355
|
+
return this.client?.getServerCapabilities();
|
|
1356
|
+
}
|
|
1357
|
+
async listTools(options) {
|
|
1358
|
+
const client = this.requireClient();
|
|
1359
|
+
const result = await client.listTools(void 0, options);
|
|
1360
|
+
return result.tools.map((tool) => {
|
|
1361
|
+
const upstreamTool = {
|
|
1362
|
+
name: tool.name,
|
|
1363
|
+
description: tool.description ?? "",
|
|
1364
|
+
inputSchema: tool.inputSchema
|
|
1365
|
+
};
|
|
1366
|
+
if (tool.outputSchema !== void 0) {
|
|
1367
|
+
upstreamTool.outputSchema = tool.outputSchema;
|
|
1368
|
+
}
|
|
1369
|
+
if (tool.annotations !== void 0) {
|
|
1370
|
+
upstreamTool.annotations = {
|
|
1371
|
+
title: tool.annotations.title,
|
|
1372
|
+
readOnlyHint: tool.annotations.readOnlyHint,
|
|
1373
|
+
destructiveHint: tool.annotations.destructiveHint,
|
|
1374
|
+
idempotentHint: tool.annotations.idempotentHint,
|
|
1375
|
+
openWorldHint: tool.annotations.openWorldHint
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
return upstreamTool;
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
async callTool(name, input, options) {
|
|
1382
|
+
const client = this.requireClient();
|
|
1383
|
+
const result = await client.callTool({ name, arguments: input }, void 0, options);
|
|
1384
|
+
return result;
|
|
1385
|
+
}
|
|
1386
|
+
async listResources(options) {
|
|
1387
|
+
const client = this.requireClient();
|
|
1388
|
+
const result = await client.listResources(void 0, options);
|
|
1389
|
+
return result.resources;
|
|
1390
|
+
}
|
|
1391
|
+
async listResourceTemplates(options) {
|
|
1392
|
+
const client = this.requireClient();
|
|
1393
|
+
const result = await client.listResourceTemplates(void 0, options);
|
|
1394
|
+
return result.resourceTemplates;
|
|
1395
|
+
}
|
|
1396
|
+
async readResource(uri, options) {
|
|
1397
|
+
const client = this.requireClient();
|
|
1398
|
+
return client.readResource({ uri }, options);
|
|
1399
|
+
}
|
|
1400
|
+
async subscribeResource(uri, options) {
|
|
1401
|
+
const client = this.requireClient();
|
|
1402
|
+
await client.subscribeResource({ uri }, options);
|
|
1403
|
+
}
|
|
1404
|
+
async unsubscribeResource(uri, options) {
|
|
1405
|
+
const client = this.requireClient();
|
|
1406
|
+
await client.unsubscribeResource({ uri }, options);
|
|
1407
|
+
}
|
|
1408
|
+
async disconnect() {
|
|
1409
|
+
if (this.client === null) {
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
await this.client.close();
|
|
1413
|
+
this.client = null;
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Sends `notifications/roots/list_changed` to the upstream, letting it know that
|
|
1417
|
+
* the host's set of filesystem roots has changed.
|
|
1418
|
+
*/
|
|
1419
|
+
async sendRootsListChanged() {
|
|
1420
|
+
const client = this.requireClient();
|
|
1421
|
+
await client.sendRootsListChanged();
|
|
1422
|
+
}
|
|
1423
|
+
registerServerRequestHandlers(client) {
|
|
1424
|
+
if (this.serverRequestHandlers.onCreateMessage !== void 0) {
|
|
1425
|
+
client.setRequestHandler(
|
|
1426
|
+
import_types4.CreateMessageRequestSchema,
|
|
1427
|
+
async (request, extra) => {
|
|
1428
|
+
return this.serverRequestHandlers.onCreateMessage(request.params, {
|
|
1429
|
+
signal: extra.signal
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
if (this.serverRequestHandlers.onElicitInput !== void 0) {
|
|
1435
|
+
client.setRequestHandler(
|
|
1436
|
+
import_types4.ElicitRequestSchema,
|
|
1437
|
+
async (request, extra) => {
|
|
1438
|
+
return this.serverRequestHandlers.onElicitInput(request.params, {
|
|
1439
|
+
signal: extra.signal
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
if (this.serverRequestHandlers.onListRoots !== void 0) {
|
|
1445
|
+
client.setRequestHandler(
|
|
1446
|
+
import_types4.ListRootsRequestSchema,
|
|
1447
|
+
async (request, extra) => {
|
|
1448
|
+
return this.serverRequestHandlers.onListRoots(request.params, {
|
|
1449
|
+
signal: extra.signal
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
requireClient() {
|
|
1456
|
+
if (this.client === null) {
|
|
1457
|
+
throw new Error("Client is not connected. Call connect() first.");
|
|
1458
|
+
}
|
|
1459
|
+
return this.client;
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
// src/diagnostics/test.ts
|
|
1464
|
+
var DESCRIPTION_MAX_LENGTH = 100;
|
|
1465
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
1466
|
+
async function test(options = {}) {
|
|
1467
|
+
const config = loadConfig({
|
|
1468
|
+
configPath: options.configPath,
|
|
1469
|
+
envFilePath: options.envFilePath
|
|
1470
|
+
});
|
|
1471
|
+
const write = options.write ?? ((chunk) => void import_node_process8.default.stdout.write(chunk));
|
|
1472
|
+
const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
1473
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1474
|
+
if (options.mcpName !== void 0) {
|
|
1475
|
+
return runSingle(config, options.mcpName, {
|
|
1476
|
+
write,
|
|
1477
|
+
now,
|
|
1478
|
+
timeoutMs,
|
|
1479
|
+
json: options.json === true
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
return runAll(config, { write, now, timeoutMs, json: options.json === true });
|
|
1483
|
+
}
|
|
1484
|
+
async function runSingle(config, mcpName2, options) {
|
|
1485
|
+
const entry = config.mcp[mcpName2];
|
|
1486
|
+
if (entry === void 0) {
|
|
1487
|
+
const available = Object.keys(config.mcp).sort().join(", ");
|
|
1488
|
+
throw new Error(`Unknown MCP "${mcpName2}". Configured MCPs: ${available || "(none)"}.`);
|
|
1489
|
+
}
|
|
1490
|
+
if (!options.json) {
|
|
1491
|
+
options.write(`Testing "${mcpName2}" (${entry.transport}, ${endpointForEntry(entry)})
|
|
1492
|
+
`);
|
|
1493
|
+
}
|
|
1494
|
+
const result = await probeOne(mcpName2, entry, options.timeoutMs, options.now);
|
|
1495
|
+
if (options.json) {
|
|
1496
|
+
options.write(`${JSON.stringify(result, null, 2)}
|
|
1497
|
+
`);
|
|
1498
|
+
} else {
|
|
1499
|
+
for (const step of result.steps) {
|
|
1500
|
+
options.write(` [${step.status}] ${step.label}
|
|
1501
|
+
`);
|
|
1502
|
+
if (step.error !== void 0) {
|
|
1503
|
+
options.write(` -> ${step.error}
|
|
1504
|
+
`);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
renderDiscoveredSurface(options.write, result);
|
|
1508
|
+
options.write(`Result: ${result.result}
|
|
1509
|
+
`);
|
|
1510
|
+
}
|
|
1511
|
+
return result.result === "PASS" ? 0 : 1;
|
|
1512
|
+
}
|
|
1513
|
+
async function runAll(config, options) {
|
|
1514
|
+
const names = Object.keys(config.mcp);
|
|
1515
|
+
const total = names.length;
|
|
1516
|
+
if (!options.json) {
|
|
1517
|
+
options.write(`Testing all configured upstreams (${total})...
|
|
1518
|
+
|
|
1519
|
+
`);
|
|
1520
|
+
}
|
|
1521
|
+
const results = [];
|
|
1522
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
1523
|
+
const name = names[index];
|
|
1524
|
+
const entry = config.mcp[name];
|
|
1525
|
+
if (!options.json) {
|
|
1526
|
+
options.write(`[${index + 1}/${total}] ${name} (${entry.transport}) ... `);
|
|
1527
|
+
}
|
|
1528
|
+
const result = await probeOne(name, entry, options.timeoutMs, options.now);
|
|
1529
|
+
results.push(result);
|
|
1530
|
+
if (!options.json) {
|
|
1531
|
+
if (result.result === "PASS") {
|
|
1532
|
+
const counts = `${result.tools?.length ?? 0} tools, ${(result.resources?.length ?? 0) + (result.resource_templates?.length ?? 0)} resources, ${result.prompts?.length ?? 0} prompts`;
|
|
1533
|
+
options.write(`PASS (${counts})
|
|
1534
|
+
`);
|
|
1535
|
+
} else {
|
|
1536
|
+
options.write(`FAIL (${result.fail_reason ?? "unknown"})
|
|
1537
|
+
`);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
const passed = results.filter((r) => r.result === "PASS").length;
|
|
1542
|
+
const failed = results.length - passed;
|
|
1543
|
+
if (options.json) {
|
|
1544
|
+
const payload = { summary: { passed, failed }, results };
|
|
1545
|
+
options.write(`${JSON.stringify(payload, null, 2)}
|
|
1546
|
+
`);
|
|
1547
|
+
} else {
|
|
1548
|
+
options.write(`
|
|
1549
|
+
Summary: ${passed} passed, ${failed} failed
|
|
1550
|
+
`);
|
|
1551
|
+
}
|
|
1552
|
+
return failed === 0 ? 0 : 1;
|
|
1553
|
+
}
|
|
1554
|
+
async function probeOne(mcpName2, entry, timeoutMs, now) {
|
|
1555
|
+
const result = {
|
|
1556
|
+
name: mcpName2,
|
|
1557
|
+
result: "PASS",
|
|
1558
|
+
transport: entry.transport,
|
|
1559
|
+
endpoint: endpointForEntry(entry),
|
|
1560
|
+
auth: deriveAuthSummary(mcpName2, entry, now),
|
|
1561
|
+
steps: []
|
|
1562
|
+
};
|
|
1563
|
+
if (entry.transport !== "stdio") {
|
|
1564
|
+
result.steps.push({ label: authStepLabel(result.auth), status: "ok" });
|
|
1565
|
+
}
|
|
1566
|
+
const clientHolder = { value: null };
|
|
1567
|
+
let timeoutHandle;
|
|
1568
|
+
const run = (async () => {
|
|
1569
|
+
const transport = createTransport(mcpName2, entry);
|
|
1570
|
+
const client = new UpstreamClient({
|
|
1571
|
+
name: mcpName2,
|
|
1572
|
+
transport,
|
|
1573
|
+
onTransportError: () => {
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
clientHolder.value = client;
|
|
1577
|
+
await client.connect();
|
|
1578
|
+
result.steps.push({ label: "Connected and initialized", status: "ok" });
|
|
1579
|
+
const caps = client.getCapabilities();
|
|
1580
|
+
result.capabilities = caps;
|
|
1581
|
+
result.steps.push({
|
|
1582
|
+
label: `Capabilities: ${describeCapabilities(caps)}`,
|
|
1583
|
+
status: "ok"
|
|
1584
|
+
});
|
|
1585
|
+
const tools = await client.listTools();
|
|
1586
|
+
result.tools = tools.map((tool) => ({
|
|
1587
|
+
name: tool.name,
|
|
1588
|
+
description: tool.description
|
|
1589
|
+
}));
|
|
1590
|
+
result.steps.push({
|
|
1591
|
+
label: `tools/list returned ${tools.length} tool${tools.length === 1 ? "" : "s"}`,
|
|
1592
|
+
status: "ok"
|
|
1593
|
+
});
|
|
1594
|
+
let resources = [];
|
|
1595
|
+
let templates = [];
|
|
1596
|
+
if (caps?.resources !== void 0) {
|
|
1597
|
+
try {
|
|
1598
|
+
resources = await client.listResources();
|
|
1599
|
+
templates = await client.listResourceTemplates();
|
|
1600
|
+
result.resources = resources.map((r) => {
|
|
1601
|
+
const out = {
|
|
1602
|
+
uri: r.uri,
|
|
1603
|
+
name: r.name
|
|
1604
|
+
};
|
|
1605
|
+
if (r.description !== void 0) out.description = r.description;
|
|
1606
|
+
return out;
|
|
1607
|
+
});
|
|
1608
|
+
result.resource_templates = templates.map((t) => {
|
|
1609
|
+
const out = {
|
|
1610
|
+
uriTemplate: t.uriTemplate,
|
|
1611
|
+
name: t.name
|
|
1612
|
+
};
|
|
1613
|
+
if (t.description !== void 0) out.description = t.description;
|
|
1614
|
+
return out;
|
|
1615
|
+
});
|
|
1616
|
+
result.steps.push({
|
|
1617
|
+
label: `resources/list returned ${resources.length} resource${resources.length === 1 ? "" : "s"}, ${templates.length} template${templates.length === 1 ? "" : "s"}`,
|
|
1618
|
+
status: "ok"
|
|
1619
|
+
});
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
result.steps.push({
|
|
1622
|
+
label: "resources/list",
|
|
1623
|
+
status: "fail",
|
|
1624
|
+
error: errorMessage(error)
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
let prompts = [];
|
|
1629
|
+
if (caps?.prompts !== void 0) {
|
|
1630
|
+
try {
|
|
1631
|
+
prompts = await client.listPrompts();
|
|
1632
|
+
result.prompts = prompts.map((p) => {
|
|
1633
|
+
const out = { name: p.name };
|
|
1634
|
+
if (p.description !== void 0) out.description = p.description;
|
|
1635
|
+
return out;
|
|
1636
|
+
});
|
|
1637
|
+
result.steps.push({
|
|
1638
|
+
label: `prompts/list returned ${prompts.length} prompt${prompts.length === 1 ? "" : "s"}`,
|
|
1639
|
+
status: "ok"
|
|
1640
|
+
});
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
result.steps.push({
|
|
1643
|
+
label: "prompts/list",
|
|
1644
|
+
status: "fail",
|
|
1645
|
+
error: errorMessage(error)
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
})();
|
|
1650
|
+
const timeout = new Promise((_, reject) => {
|
|
1651
|
+
timeoutHandle = setTimeout(() => {
|
|
1652
|
+
reject(new TestTimeoutError(timeoutMs));
|
|
1653
|
+
}, timeoutMs);
|
|
1654
|
+
});
|
|
1655
|
+
try {
|
|
1656
|
+
await Promise.race([run, timeout]);
|
|
1657
|
+
} catch (error) {
|
|
1658
|
+
result.result = "FAIL";
|
|
1659
|
+
result.fail_reason = failReason(error, mcpName2);
|
|
1660
|
+
result.steps.push({
|
|
1661
|
+
label: `aborted: ${result.fail_reason}`,
|
|
1662
|
+
status: "fail",
|
|
1663
|
+
error: errorMessage(error)
|
|
1664
|
+
});
|
|
1665
|
+
} finally {
|
|
1666
|
+
if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
|
|
1667
|
+
const connected = clientHolder.value;
|
|
1668
|
+
if (connected !== null) {
|
|
1669
|
+
try {
|
|
1670
|
+
await connected.disconnect();
|
|
1671
|
+
} catch {
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
if (result.result === "PASS" && result.steps.some((s) => s.status === "fail")) {
|
|
1676
|
+
result.result = "FAIL";
|
|
1677
|
+
const failed = result.steps.find((s) => s.status === "fail");
|
|
1678
|
+
result.fail_reason = failed?.error ?? failed?.label ?? "unknown";
|
|
1679
|
+
}
|
|
1680
|
+
return result;
|
|
1681
|
+
}
|
|
1682
|
+
var TestTimeoutError = class extends Error {
|
|
1683
|
+
constructor(timeoutMs) {
|
|
1684
|
+
super(`Test timed out after ${timeoutMs}ms`);
|
|
1685
|
+
this.name = "TestTimeoutError";
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
function endpointForEntry(entry) {
|
|
1689
|
+
if (entry.transport === "stdio") {
|
|
1690
|
+
const args = (entry.args ?? []).join(" ");
|
|
1691
|
+
return args.length > 0 ? `${entry.command} ${args}` : entry.command;
|
|
372
1692
|
}
|
|
373
|
-
return
|
|
1693
|
+
return entry.url;
|
|
374
1694
|
}
|
|
375
|
-
function
|
|
376
|
-
if (
|
|
377
|
-
|
|
1695
|
+
function deriveAuthSummary(mcpName2, entry, now) {
|
|
1696
|
+
if (entry.transport === "stdio") return { kind: "n/a" };
|
|
1697
|
+
const keychain = new KeychainStore(mcpName2, entry.url);
|
|
1698
|
+
const blob = keychain.get();
|
|
1699
|
+
if (blob !== void 0) {
|
|
1700
|
+
return {
|
|
1701
|
+
kind: "oauth",
|
|
1702
|
+
status: "valid",
|
|
1703
|
+
expiresInSeconds: blob.expires_at - now()
|
|
1704
|
+
};
|
|
378
1705
|
}
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
1706
|
+
const hasHeader = entry.headers !== void 0 && Object.keys(entry.headers).some((k) => k.toLowerCase() === "authorization");
|
|
1707
|
+
if (hasHeader) return { kind: "header" };
|
|
1708
|
+
return { kind: "oauth", status: "missing" };
|
|
1709
|
+
}
|
|
1710
|
+
function authStepLabel(auth2) {
|
|
1711
|
+
switch (auth2.kind) {
|
|
1712
|
+
case "n/a":
|
|
1713
|
+
return "(no auth applicable)";
|
|
1714
|
+
case "header":
|
|
1715
|
+
return "Static Authorization header present";
|
|
1716
|
+
case "oauth":
|
|
1717
|
+
if (auth2.status === "missing") return "No cached OAuth token";
|
|
1718
|
+
return `OAuth token present (expires in ${humanizeDuration(auth2.expiresInSeconds ?? 0)})`;
|
|
383
1719
|
}
|
|
384
|
-
return DEFAULT_ENV_MODE;
|
|
385
1720
|
}
|
|
386
|
-
function
|
|
387
|
-
|
|
1721
|
+
function describeCapabilities(caps) {
|
|
1722
|
+
if (caps === void 0) return "(none advertised)";
|
|
1723
|
+
const parts = [];
|
|
1724
|
+
for (const [name, value] of Object.entries(caps)) {
|
|
1725
|
+
if (value === void 0 || value === null) continue;
|
|
1726
|
+
if (typeof value === "object" && Object.keys(value).length > 0) {
|
|
1727
|
+
const flags = Object.entries(value).filter(([, v]) => v === true).map(([k]) => k).join(",");
|
|
1728
|
+
parts.push(flags.length > 0 ? `${name}(${flags})` : name);
|
|
1729
|
+
} else {
|
|
1730
|
+
parts.push(name);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return parts.length > 0 ? parts.join(", ") : "(none advertised)";
|
|
1734
|
+
}
|
|
1735
|
+
function failReason(error, mcpName2) {
|
|
1736
|
+
if (isAuthRequiredError(error)) {
|
|
1737
|
+
return `auth required: run \`dynmcp login ${mcpName2}\``;
|
|
1738
|
+
}
|
|
1739
|
+
if (error instanceof TestTimeoutError) return error.message;
|
|
1740
|
+
if (error instanceof Error) return error.message.split("\n")[0] ?? "unknown error";
|
|
1741
|
+
return String(error);
|
|
1742
|
+
}
|
|
1743
|
+
function errorMessage(error) {
|
|
1744
|
+
if (error instanceof Error) return error.message;
|
|
1745
|
+
return String(error);
|
|
1746
|
+
}
|
|
1747
|
+
function renderDiscoveredSurface(write, result) {
|
|
1748
|
+
if (result.result !== "PASS") return;
|
|
1749
|
+
const sections = [
|
|
1750
|
+
[
|
|
1751
|
+
`Tools (${result.tools?.length ?? 0})`,
|
|
1752
|
+
result.tools && result.tools.length > 0 ? () => {
|
|
1753
|
+
const sorted = [...result.tools ?? []].sort((a, b) => a.name.localeCompare(b.name));
|
|
1754
|
+
for (const tool of sorted) {
|
|
1755
|
+
write(` - ${tool.name}: ${truncate(tool.description, DESCRIPTION_MAX_LENGTH)}
|
|
1756
|
+
`);
|
|
1757
|
+
}
|
|
1758
|
+
} : void 0
|
|
1759
|
+
],
|
|
1760
|
+
[
|
|
1761
|
+
`Resources (${result.resources?.length ?? 0})`,
|
|
1762
|
+
result.resources && result.resources.length > 0 ? () => {
|
|
1763
|
+
const sorted = [...result.resources ?? []].sort((a, b) => a.uri.localeCompare(b.uri));
|
|
1764
|
+
for (const r of sorted) {
|
|
1765
|
+
const tail = r.description ?? r.name;
|
|
1766
|
+
write(` - ${r.uri}: ${truncate(tail, DESCRIPTION_MAX_LENGTH)}
|
|
1767
|
+
`);
|
|
1768
|
+
}
|
|
1769
|
+
} : void 0
|
|
1770
|
+
],
|
|
1771
|
+
[
|
|
1772
|
+
`Resource templates (${result.resource_templates?.length ?? 0})`,
|
|
1773
|
+
result.resource_templates && result.resource_templates.length > 0 ? () => {
|
|
1774
|
+
const sorted = [...result.resource_templates ?? []].sort(
|
|
1775
|
+
(a, b) => a.uriTemplate.localeCompare(b.uriTemplate)
|
|
1776
|
+
);
|
|
1777
|
+
for (const t of sorted) {
|
|
1778
|
+
const tail = t.description ?? t.name;
|
|
1779
|
+
write(` - ${t.uriTemplate}: ${truncate(tail, DESCRIPTION_MAX_LENGTH)}
|
|
1780
|
+
`);
|
|
1781
|
+
}
|
|
1782
|
+
} : void 0
|
|
1783
|
+
],
|
|
1784
|
+
[
|
|
1785
|
+
`Prompts (${result.prompts?.length ?? 0})`,
|
|
1786
|
+
result.prompts && result.prompts.length > 0 ? () => {
|
|
1787
|
+
const sorted = [...result.prompts ?? []].sort((a, b) => a.name.localeCompare(b.name));
|
|
1788
|
+
for (const p of sorted) {
|
|
1789
|
+
const tail = p.description ?? "";
|
|
1790
|
+
write(
|
|
1791
|
+
` - ${p.name}${tail.length > 0 ? `: ${truncate(tail, DESCRIPTION_MAX_LENGTH)}` : ""}
|
|
1792
|
+
`
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
} : void 0
|
|
1796
|
+
]
|
|
1797
|
+
];
|
|
1798
|
+
for (const [header, render] of sections) {
|
|
1799
|
+
if (render === void 0) continue;
|
|
1800
|
+
write(`
|
|
1801
|
+
${header}:
|
|
1802
|
+
`);
|
|
1803
|
+
render();
|
|
1804
|
+
}
|
|
388
1805
|
}
|
|
389
1806
|
|
|
390
|
-
// src/
|
|
391
|
-
var
|
|
1807
|
+
// src/proxy/index.ts
|
|
1808
|
+
var import_node_process11 = __toESM(require("process"), 1);
|
|
1809
|
+
var import_stdio3 = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
392
1810
|
|
|
393
1811
|
// src/proxy/orchestrator.ts
|
|
394
|
-
var
|
|
1812
|
+
var import_node_process9 = __toESM(require("process"), 1);
|
|
395
1813
|
|
|
396
1814
|
// src/proxy/capability-aggregator.ts
|
|
397
1815
|
function aggregateCapabilities(upstreams) {
|
|
@@ -799,301 +2217,97 @@ var ToolCatalog = class _ToolCatalog {
|
|
|
799
2217
|
return new _ToolCatalog(toolMap, description2);
|
|
800
2218
|
}
|
|
801
2219
|
getToolDetails(toolName) {
|
|
802
|
-
const tool = this.tools.get(toolName);
|
|
803
|
-
if (tool === void 0) {
|
|
804
|
-
const sortedNames = [...this.tools.keys()].sort().join(", ");
|
|
805
|
-
return `Unknown tool: "${toolName}". Available tools: ${sortedNames}`;
|
|
806
|
-
}
|
|
807
|
-
return buildToolDetailsString(toolName, tool);
|
|
808
|
-
}
|
|
809
|
-
};
|
|
810
|
-
function buildFlatDescription(tools) {
|
|
811
|
-
const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
812
|
-
const toolLines = sortedTools.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
|
|
813
|
-
return `${DISCOVER_TOOL_PREAMBLE}
|
|
814
|
-
|
|
815
|
-
<tools>
|
|
816
|
-
${toolLines}
|
|
817
|
-
</tools>`;
|
|
818
|
-
}
|
|
819
|
-
function buildGroupedDescription(groups, lazyDescriptions) {
|
|
820
|
-
const parts = [DISCOVER_TOOL_PREAMBLE];
|
|
821
|
-
if (lazyDescriptions.size > 0) {
|
|
822
|
-
parts.push(DYNAMIC_DISCOVERY_PREAMBLE);
|
|
823
|
-
parts.push(buildMcpServersBlock(lazyDescriptions));
|
|
824
|
-
}
|
|
825
|
-
if (groups.size > 0) {
|
|
826
|
-
parts.push(buildToolsBlock(groups));
|
|
827
|
-
} else if (lazyDescriptions.size > 0) {
|
|
828
|
-
parts.push(NO_TOOLS_LOADED_FOOTER);
|
|
829
|
-
} else {
|
|
830
|
-
parts.push("<tools>\n</tools>");
|
|
831
|
-
}
|
|
832
|
-
return parts.join("\n\n");
|
|
833
|
-
}
|
|
834
|
-
function buildToolsBlock(groups) {
|
|
835
|
-
const sortedMcpNames = [...groups.keys()].sort();
|
|
836
|
-
const sections = sortedMcpNames.map((mcpName2) => {
|
|
837
|
-
const tools = groups.get(mcpName2);
|
|
838
|
-
const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
839
|
-
const toolLines = sortedTools.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
|
|
840
|
-
return `${mcpName2}:
|
|
841
|
-
${toolLines}`;
|
|
842
|
-
});
|
|
843
|
-
return `<tools>
|
|
844
|
-
${sections.join("\n\n")}
|
|
845
|
-
</tools>`;
|
|
846
|
-
}
|
|
847
|
-
function buildMcpServersBlock(lazyDescriptions) {
|
|
848
|
-
const lines = [...lazyDescriptions].map(([name, desc]) => `- ${name}: ${desc}`).join("\n");
|
|
849
|
-
return `<mcp_servers>
|
|
850
|
-
${lines}
|
|
851
|
-
</mcp_servers>`;
|
|
852
|
-
}
|
|
853
|
-
function buildToolDetailsString(displayName, tool) {
|
|
854
|
-
const lines = [
|
|
855
|
-
`Tool: ${displayName}`,
|
|
856
|
-
`Description: ${tool.description}`,
|
|
857
|
-
"",
|
|
858
|
-
"Input Schema:",
|
|
859
|
-
JSON.stringify(tool.inputSchema, null, 2)
|
|
860
|
-
];
|
|
861
|
-
if (tool.outputSchema !== void 0) {
|
|
862
|
-
lines.push("", "Output Schema:", JSON.stringify(tool.outputSchema, null, 2));
|
|
863
|
-
}
|
|
864
|
-
const annotationLines = buildAnnotationLines(tool);
|
|
865
|
-
if (annotationLines.length > 0) {
|
|
866
|
-
lines.push("", "Annotations:", ...annotationLines);
|
|
867
|
-
}
|
|
868
|
-
return lines.join("\n");
|
|
869
|
-
}
|
|
870
|
-
function buildAnnotationLines(tool) {
|
|
871
|
-
if (tool.annotations === void 0) {
|
|
872
|
-
return [];
|
|
873
|
-
}
|
|
874
|
-
const { annotations } = tool;
|
|
875
|
-
const lines = [];
|
|
876
|
-
if (annotations.title !== void 0) {
|
|
877
|
-
lines.push(`- title: ${annotations.title}`);
|
|
878
|
-
}
|
|
879
|
-
if (annotations.readOnlyHint !== void 0) {
|
|
880
|
-
lines.push(`- readOnlyHint: ${annotations.readOnlyHint}`);
|
|
881
|
-
}
|
|
882
|
-
if (annotations.destructiveHint !== void 0) {
|
|
883
|
-
lines.push(`- destructiveHint: ${annotations.destructiveHint}`);
|
|
884
|
-
}
|
|
885
|
-
if (annotations.idempotentHint !== void 0) {
|
|
886
|
-
lines.push(`- idempotentHint: ${annotations.idempotentHint}`);
|
|
887
|
-
}
|
|
888
|
-
if (annotations.openWorldHint !== void 0) {
|
|
889
|
-
lines.push(`- openWorldHint: ${annotations.openWorldHint}`);
|
|
890
|
-
}
|
|
891
|
-
return lines;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// src/proxy/upstream-client.ts
|
|
895
|
-
var import_node_process3 = __toESM(require("process"), 1);
|
|
896
|
-
var import_client = require("@modelcontextprotocol/sdk/client/index.js");
|
|
897
|
-
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
898
|
-
var UpstreamClient = class {
|
|
899
|
-
transport;
|
|
900
|
-
onTransportError;
|
|
901
|
-
notificationHandlers;
|
|
902
|
-
serverRequestHandlers;
|
|
903
|
-
client = null;
|
|
904
|
-
constructor({
|
|
905
|
-
name,
|
|
906
|
-
transport,
|
|
907
|
-
onTransportError,
|
|
908
|
-
notifications,
|
|
909
|
-
serverRequests
|
|
910
|
-
}) {
|
|
911
|
-
this.transport = transport;
|
|
912
|
-
this.notificationHandlers = notifications ?? {};
|
|
913
|
-
this.serverRequestHandlers = serverRequests ?? {};
|
|
914
|
-
this.onTransportError = onTransportError ?? ((error) => {
|
|
915
|
-
import_node_process3.default.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
|
|
916
|
-
`);
|
|
917
|
-
});
|
|
918
|
-
}
|
|
919
|
-
async connect() {
|
|
920
|
-
this.transport.onerror = this.onTransportError;
|
|
921
|
-
this.client = new import_client.Client(
|
|
922
|
-
{ name: "dynamic-discovery-mcp", version: "1.0.0" },
|
|
923
|
-
{
|
|
924
|
-
capabilities: {
|
|
925
|
-
// Declare every client-side capability the proxy may relay on behalf of the host.
|
|
926
|
-
// Actual reachability of each feature depends on what the host supports — if the
|
|
927
|
-
// host does not support sampling, for instance, the host call returns an error
|
|
928
|
-
// which we forward back to the upstream verbatim.
|
|
929
|
-
sampling: {},
|
|
930
|
-
elicitation: {},
|
|
931
|
-
roots: { listChanged: true }
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
);
|
|
935
|
-
this.registerServerRequestHandlers(this.client);
|
|
936
|
-
if (this.notificationHandlers.onToolsListChanged !== void 0) {
|
|
937
|
-
this.client.setNotificationHandler(import_types.ToolListChangedNotificationSchema, async () => {
|
|
938
|
-
await this.notificationHandlers.onToolsListChanged?.();
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
if (this.notificationHandlers.onResourcesListChanged !== void 0) {
|
|
942
|
-
this.client.setNotificationHandler(import_types.ResourceListChangedNotificationSchema, async () => {
|
|
943
|
-
await this.notificationHandlers.onResourcesListChanged?.();
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
if (this.notificationHandlers.onResourceUpdated !== void 0) {
|
|
947
|
-
this.client.setNotificationHandler(import_types.ResourceUpdatedNotificationSchema, async (notification) => {
|
|
948
|
-
await this.notificationHandlers.onResourceUpdated?.({ uri: notification.params.uri });
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
if (this.notificationHandlers.onPromptsListChanged !== void 0) {
|
|
952
|
-
this.client.setNotificationHandler(import_types.PromptListChangedNotificationSchema, async () => {
|
|
953
|
-
await this.notificationHandlers.onPromptsListChanged?.();
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
if (this.notificationHandlers.onLogMessage !== void 0) {
|
|
957
|
-
this.client.setNotificationHandler(import_types.LoggingMessageNotificationSchema, async (notification) => {
|
|
958
|
-
await this.notificationHandlers.onLogMessage?.(notification.params);
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
await this.client.connect(this.transport);
|
|
962
|
-
}
|
|
963
|
-
async setLoggingLevel(level, options) {
|
|
964
|
-
const client = this.requireClient();
|
|
965
|
-
await client.setLoggingLevel(level, options);
|
|
966
|
-
}
|
|
967
|
-
async listPrompts(options) {
|
|
968
|
-
const client = this.requireClient();
|
|
969
|
-
const result = await client.listPrompts(void 0, options);
|
|
970
|
-
return result.prompts;
|
|
971
|
-
}
|
|
972
|
-
async getPrompt(name, args, options) {
|
|
973
|
-
const client = this.requireClient();
|
|
974
|
-
const params = { name };
|
|
975
|
-
if (args !== void 0) {
|
|
976
|
-
params.arguments = args;
|
|
977
|
-
}
|
|
978
|
-
return client.getPrompt(params, options);
|
|
979
|
-
}
|
|
980
|
-
async complete(params, options) {
|
|
981
|
-
const client = this.requireClient();
|
|
982
|
-
return client.complete(params, options);
|
|
983
|
-
}
|
|
984
|
-
/**
|
|
985
|
-
* Returns the capabilities advertised by the upstream server during initialize.
|
|
986
|
-
* Returns `undefined` if the client is not connected, or if the SDK has not yet
|
|
987
|
-
* recorded the server's capabilities (e.g. during a partially-completed handshake).
|
|
988
|
-
*/
|
|
989
|
-
getCapabilities() {
|
|
990
|
-
return this.client?.getServerCapabilities();
|
|
991
|
-
}
|
|
992
|
-
async listTools(options) {
|
|
993
|
-
const client = this.requireClient();
|
|
994
|
-
const result = await client.listTools(void 0, options);
|
|
995
|
-
return result.tools.map((tool) => {
|
|
996
|
-
const upstreamTool = {
|
|
997
|
-
name: tool.name,
|
|
998
|
-
description: tool.description ?? "",
|
|
999
|
-
inputSchema: tool.inputSchema
|
|
1000
|
-
};
|
|
1001
|
-
if (tool.outputSchema !== void 0) {
|
|
1002
|
-
upstreamTool.outputSchema = tool.outputSchema;
|
|
1003
|
-
}
|
|
1004
|
-
if (tool.annotations !== void 0) {
|
|
1005
|
-
upstreamTool.annotations = {
|
|
1006
|
-
title: tool.annotations.title,
|
|
1007
|
-
readOnlyHint: tool.annotations.readOnlyHint,
|
|
1008
|
-
destructiveHint: tool.annotations.destructiveHint,
|
|
1009
|
-
idempotentHint: tool.annotations.idempotentHint,
|
|
1010
|
-
openWorldHint: tool.annotations.openWorldHint
|
|
1011
|
-
};
|
|
1012
|
-
}
|
|
1013
|
-
return upstreamTool;
|
|
1014
|
-
});
|
|
2220
|
+
const tool = this.tools.get(toolName);
|
|
2221
|
+
if (tool === void 0) {
|
|
2222
|
+
const sortedNames = [...this.tools.keys()].sort().join(", ");
|
|
2223
|
+
return `Unknown tool: "${toolName}". Available tools: ${sortedNames}`;
|
|
2224
|
+
}
|
|
2225
|
+
return buildToolDetailsString(toolName, tool);
|
|
1015
2226
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
2227
|
+
};
|
|
2228
|
+
function buildFlatDescription(tools) {
|
|
2229
|
+
const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
2230
|
+
const toolLines = sortedTools.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
|
|
2231
|
+
return `${DISCOVER_TOOL_PREAMBLE}
|
|
2232
|
+
|
|
2233
|
+
<tools>
|
|
2234
|
+
${toolLines}
|
|
2235
|
+
</tools>`;
|
|
2236
|
+
}
|
|
2237
|
+
function buildGroupedDescription(groups, lazyDescriptions) {
|
|
2238
|
+
const parts = [DISCOVER_TOOL_PREAMBLE];
|
|
2239
|
+
if (lazyDescriptions.size > 0) {
|
|
2240
|
+
parts.push(DYNAMIC_DISCOVERY_PREAMBLE);
|
|
2241
|
+
parts.push(buildMcpServersBlock(lazyDescriptions));
|
|
1020
2242
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
2243
|
+
if (groups.size > 0) {
|
|
2244
|
+
parts.push(buildToolsBlock(groups));
|
|
2245
|
+
} else if (lazyDescriptions.size > 0) {
|
|
2246
|
+
parts.push(NO_TOOLS_LOADED_FOOTER);
|
|
2247
|
+
} else {
|
|
2248
|
+
parts.push("<tools>\n</tools>");
|
|
1025
2249
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
2250
|
+
return parts.join("\n\n");
|
|
2251
|
+
}
|
|
2252
|
+
function buildToolsBlock(groups) {
|
|
2253
|
+
const sortedMcpNames = [...groups.keys()].sort();
|
|
2254
|
+
const sections = sortedMcpNames.map((mcpName2) => {
|
|
2255
|
+
const tools = groups.get(mcpName2);
|
|
2256
|
+
const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
2257
|
+
const toolLines = sortedTools.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
|
|
2258
|
+
return `${mcpName2}:
|
|
2259
|
+
${toolLines}`;
|
|
2260
|
+
});
|
|
2261
|
+
return `<tools>
|
|
2262
|
+
${sections.join("\n\n")}
|
|
2263
|
+
</tools>`;
|
|
2264
|
+
}
|
|
2265
|
+
function buildMcpServersBlock(lazyDescriptions) {
|
|
2266
|
+
const lines = [...lazyDescriptions].map(([name, desc]) => `- ${name}: ${desc}`).join("\n");
|
|
2267
|
+
return `<mcp_servers>
|
|
2268
|
+
${lines}
|
|
2269
|
+
</mcp_servers>`;
|
|
2270
|
+
}
|
|
2271
|
+
function buildToolDetailsString(displayName, tool) {
|
|
2272
|
+
const lines = [
|
|
2273
|
+
`Tool: ${displayName}`,
|
|
2274
|
+
`Description: ${tool.description}`,
|
|
2275
|
+
"",
|
|
2276
|
+
"Input Schema:",
|
|
2277
|
+
JSON.stringify(tool.inputSchema, null, 2)
|
|
2278
|
+
];
|
|
2279
|
+
if (tool.outputSchema !== void 0) {
|
|
2280
|
+
lines.push("", "Output Schema:", JSON.stringify(tool.outputSchema, null, 2));
|
|
1030
2281
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
2282
|
+
const annotationLines = buildAnnotationLines(tool);
|
|
2283
|
+
if (annotationLines.length > 0) {
|
|
2284
|
+
lines.push("", "Annotations:", ...annotationLines);
|
|
1034
2285
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
2286
|
+
return lines.join("\n");
|
|
2287
|
+
}
|
|
2288
|
+
function buildAnnotationLines(tool) {
|
|
2289
|
+
if (tool.annotations === void 0) {
|
|
2290
|
+
return [];
|
|
1038
2291
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
2292
|
+
const { annotations } = tool;
|
|
2293
|
+
const lines = [];
|
|
2294
|
+
if (annotations.title !== void 0) {
|
|
2295
|
+
lines.push(`- title: ${annotations.title}`);
|
|
1042
2296
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
await this.client.close();
|
|
1048
|
-
this.client = null;
|
|
2297
|
+
if (annotations.readOnlyHint !== void 0) {
|
|
2298
|
+
lines.push(`- readOnlyHint: ${annotations.readOnlyHint}`);
|
|
1049
2299
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
* the host's set of filesystem roots has changed.
|
|
1053
|
-
*/
|
|
1054
|
-
async sendRootsListChanged() {
|
|
1055
|
-
const client = this.requireClient();
|
|
1056
|
-
await client.sendRootsListChanged();
|
|
2300
|
+
if (annotations.destructiveHint !== void 0) {
|
|
2301
|
+
lines.push(`- destructiveHint: ${annotations.destructiveHint}`);
|
|
1057
2302
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
client.setRequestHandler(
|
|
1061
|
-
import_types.CreateMessageRequestSchema,
|
|
1062
|
-
async (request, extra) => {
|
|
1063
|
-
return this.serverRequestHandlers.onCreateMessage(request.params, {
|
|
1064
|
-
signal: extra.signal
|
|
1065
|
-
});
|
|
1066
|
-
}
|
|
1067
|
-
);
|
|
1068
|
-
}
|
|
1069
|
-
if (this.serverRequestHandlers.onElicitInput !== void 0) {
|
|
1070
|
-
client.setRequestHandler(
|
|
1071
|
-
import_types.ElicitRequestSchema,
|
|
1072
|
-
async (request, extra) => {
|
|
1073
|
-
return this.serverRequestHandlers.onElicitInput(request.params, {
|
|
1074
|
-
signal: extra.signal
|
|
1075
|
-
});
|
|
1076
|
-
}
|
|
1077
|
-
);
|
|
1078
|
-
}
|
|
1079
|
-
if (this.serverRequestHandlers.onListRoots !== void 0) {
|
|
1080
|
-
client.setRequestHandler(
|
|
1081
|
-
import_types.ListRootsRequestSchema,
|
|
1082
|
-
async (request, extra) => {
|
|
1083
|
-
return this.serverRequestHandlers.onListRoots(request.params, {
|
|
1084
|
-
signal: extra.signal
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
|
-
);
|
|
1088
|
-
}
|
|
2303
|
+
if (annotations.idempotentHint !== void 0) {
|
|
2304
|
+
lines.push(`- idempotentHint: ${annotations.idempotentHint}`);
|
|
1089
2305
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
throw new Error("Client is not connected. Call connect() first.");
|
|
1093
|
-
}
|
|
1094
|
-
return this.client;
|
|
2306
|
+
if (annotations.openWorldHint !== void 0) {
|
|
2307
|
+
lines.push(`- openWorldHint: ${annotations.openWorldHint}`);
|
|
1095
2308
|
}
|
|
1096
|
-
|
|
2309
|
+
return lines;
|
|
2310
|
+
}
|
|
1097
2311
|
|
|
1098
2312
|
// src/proxy/upstream-registry.ts
|
|
1099
2313
|
var UpstreamRegistry = class {
|
|
@@ -1346,6 +2560,9 @@ var Orchestrator = class {
|
|
|
1346
2560
|
}
|
|
1347
2561
|
} catch (error) {
|
|
1348
2562
|
await this.registry.deleteOne(mcpName2);
|
|
2563
|
+
if (isAuthRequiredError(error)) {
|
|
2564
|
+
throw error;
|
|
2565
|
+
}
|
|
1349
2566
|
const failures = this.lazyRegistry.recordFailure(mcpName2);
|
|
1350
2567
|
if (failures >= MAX_LOAD_ATTEMPTS) {
|
|
1351
2568
|
this.lazyRegistry.take(mcpName2);
|
|
@@ -1592,7 +2809,7 @@ var Orchestrator = class {
|
|
|
1592
2809
|
targets.push(
|
|
1593
2810
|
action(client).catch((error) => {
|
|
1594
2811
|
const message = error instanceof Error ? error.message : String(error);
|
|
1595
|
-
|
|
2812
|
+
import_node_process9.default.stderr.write(`dynmcp: ${label} failed for "${mcpName2}": ${message}
|
|
1596
2813
|
`);
|
|
1597
2814
|
})
|
|
1598
2815
|
);
|
|
@@ -1617,13 +2834,13 @@ function splitNamespacedName(namespacedName, knownMcpNames) {
|
|
|
1617
2834
|
}
|
|
1618
2835
|
function logCollisions(resourceRouter, promptRouter) {
|
|
1619
2836
|
for (const collision of resourceRouter.collisions()) {
|
|
1620
|
-
|
|
2837
|
+
import_node_process9.default.stderr.write(
|
|
1621
2838
|
`dynmcp: resource URI collision: "${collision.uri}" is provided by "${collision.chosen}" and "${collision.shadowed}"; routing to "${collision.chosen}".
|
|
1622
2839
|
`
|
|
1623
2840
|
);
|
|
1624
2841
|
}
|
|
1625
2842
|
for (const collision of promptRouter.collisions()) {
|
|
1626
|
-
|
|
2843
|
+
import_node_process9.default.stderr.write(
|
|
1627
2844
|
`dynmcp: prompt name collision: "${collision.name}" is provided by "${collision.chosen}" and "${collision.shadowed}"; routing to "${collision.chosen}".
|
|
1628
2845
|
`
|
|
1629
2846
|
);
|
|
@@ -1631,10 +2848,10 @@ function logCollisions(resourceRouter, promptRouter) {
|
|
|
1631
2848
|
}
|
|
1632
2849
|
|
|
1633
2850
|
// src/proxy/server.ts
|
|
1634
|
-
var
|
|
2851
|
+
var import_node_process10 = __toESM(require("process"), 1);
|
|
1635
2852
|
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
1636
|
-
var
|
|
1637
|
-
var
|
|
2853
|
+
var import_stdio2 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
2854
|
+
var import_types5 = require("@modelcontextprotocol/sdk/types.js");
|
|
1638
2855
|
var import_zod3 = require("zod");
|
|
1639
2856
|
var DISCOVER_TOOL_NAME = "discover_tool";
|
|
1640
2857
|
var USE_TOOL_NAME = "use_tool";
|
|
@@ -1726,7 +2943,7 @@ var ProxyServer = class {
|
|
|
1726
2943
|
}
|
|
1727
2944
|
if (this.onRootsListChangedCallback !== void 0) {
|
|
1728
2945
|
const callback = this.onRootsListChangedCallback;
|
|
1729
|
-
server.setNotificationHandler(
|
|
2946
|
+
server.setNotificationHandler(import_types5.RootsListChangedNotificationSchema, async () => {
|
|
1730
2947
|
await callback();
|
|
1731
2948
|
});
|
|
1732
2949
|
}
|
|
@@ -1764,8 +2981,8 @@ var ProxyServer = class {
|
|
|
1764
2981
|
}
|
|
1765
2982
|
async start() {
|
|
1766
2983
|
const server = this.buildServer();
|
|
1767
|
-
const transport = new
|
|
1768
|
-
|
|
2984
|
+
const transport = new import_stdio2.StdioServerTransport();
|
|
2985
|
+
import_node_process10.default.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
|
|
1769
2986
|
await server.connect(transport);
|
|
1770
2987
|
}
|
|
1771
2988
|
/**
|
|
@@ -1832,7 +3049,7 @@ var ProxyServer = class {
|
|
|
1832
3049
|
return options;
|
|
1833
3050
|
}
|
|
1834
3051
|
registerToolHandlers(server) {
|
|
1835
|
-
server.setRequestHandler(
|
|
3052
|
+
server.setRequestHandler(import_types5.ListToolsRequestSchema, async () => {
|
|
1836
3053
|
const tools = [
|
|
1837
3054
|
{
|
|
1838
3055
|
name: DISCOVER_TOOL_NAME,
|
|
@@ -1855,7 +3072,7 @@ var ProxyServer = class {
|
|
|
1855
3072
|
return { tools };
|
|
1856
3073
|
});
|
|
1857
3074
|
server.setRequestHandler(
|
|
1858
|
-
|
|
3075
|
+
import_types5.CallToolRequestSchema,
|
|
1859
3076
|
async (request, extra) => {
|
|
1860
3077
|
const { name, arguments: rawArgs } = request.params;
|
|
1861
3078
|
const catalog = this.catalog();
|
|
@@ -1906,28 +3123,28 @@ var ProxyServer = class {
|
|
|
1906
3123
|
}
|
|
1907
3124
|
registerResourceHandlers(server, callbacks) {
|
|
1908
3125
|
server.setRequestHandler(
|
|
1909
|
-
|
|
3126
|
+
import_types5.ListResourcesRequestSchema,
|
|
1910
3127
|
async () => ({
|
|
1911
3128
|
resources: callbacks.listResources()
|
|
1912
3129
|
})
|
|
1913
3130
|
);
|
|
1914
3131
|
server.setRequestHandler(
|
|
1915
|
-
|
|
3132
|
+
import_types5.ListResourceTemplatesRequestSchema,
|
|
1916
3133
|
async () => ({
|
|
1917
3134
|
resourceTemplates: callbacks.listResourceTemplates()
|
|
1918
3135
|
})
|
|
1919
3136
|
);
|
|
1920
3137
|
server.setRequestHandler(
|
|
1921
|
-
|
|
3138
|
+
import_types5.ReadResourceRequestSchema,
|
|
1922
3139
|
async (request, extra) => {
|
|
1923
3140
|
return callbacks.readResource(request.params.uri, this.buildCallOptions(request, extra));
|
|
1924
3141
|
}
|
|
1925
3142
|
);
|
|
1926
|
-
server.setRequestHandler(
|
|
3143
|
+
server.setRequestHandler(import_types5.SubscribeRequestSchema, async (request, extra) => {
|
|
1927
3144
|
await callbacks.subscribeResource(request.params.uri, this.buildCallOptions(request, extra));
|
|
1928
3145
|
return {};
|
|
1929
3146
|
});
|
|
1930
|
-
server.setRequestHandler(
|
|
3147
|
+
server.setRequestHandler(import_types5.UnsubscribeRequestSchema, async (request, extra) => {
|
|
1931
3148
|
await callbacks.unsubscribeResource(
|
|
1932
3149
|
request.params.uri,
|
|
1933
3150
|
this.buildCallOptions(request, extra)
|
|
@@ -1937,13 +3154,13 @@ var ProxyServer = class {
|
|
|
1937
3154
|
}
|
|
1938
3155
|
registerPromptHandlers(server, callbacks) {
|
|
1939
3156
|
server.setRequestHandler(
|
|
1940
|
-
|
|
3157
|
+
import_types5.ListPromptsRequestSchema,
|
|
1941
3158
|
async () => ({
|
|
1942
3159
|
prompts: callbacks.listPrompts()
|
|
1943
3160
|
})
|
|
1944
3161
|
);
|
|
1945
3162
|
server.setRequestHandler(
|
|
1946
|
-
|
|
3163
|
+
import_types5.GetPromptRequestSchema,
|
|
1947
3164
|
async (request, extra) => {
|
|
1948
3165
|
return callbacks.getPrompt(
|
|
1949
3166
|
request.params.name,
|
|
@@ -1955,14 +3172,14 @@ var ProxyServer = class {
|
|
|
1955
3172
|
}
|
|
1956
3173
|
registerCompletionHandler(server, callback) {
|
|
1957
3174
|
server.setRequestHandler(
|
|
1958
|
-
|
|
3175
|
+
import_types5.CompleteRequestSchema,
|
|
1959
3176
|
async (request, extra) => {
|
|
1960
3177
|
return callback(request.params, this.buildCallOptions(request, extra));
|
|
1961
3178
|
}
|
|
1962
3179
|
);
|
|
1963
3180
|
}
|
|
1964
3181
|
registerLoggingHandler(server, callback) {
|
|
1965
|
-
server.setRequestHandler(
|
|
3182
|
+
server.setRequestHandler(import_types5.SetLevelRequestSchema, async (request, extra) => {
|
|
1966
3183
|
await callback(request.params.level, this.buildCallOptions(request, extra));
|
|
1967
3184
|
return {};
|
|
1968
3185
|
});
|
|
@@ -1978,35 +3195,6 @@ var ProxyServer = class {
|
|
|
1978
3195
|
}
|
|
1979
3196
|
};
|
|
1980
3197
|
|
|
1981
|
-
// src/proxy/transport-factory.ts
|
|
1982
|
-
var import_stdio2 = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
1983
|
-
var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
1984
|
-
var import_sse = require("@modelcontextprotocol/sdk/client/sse.js");
|
|
1985
|
-
function createTransport(config) {
|
|
1986
|
-
switch (config.transport) {
|
|
1987
|
-
case "stdio":
|
|
1988
|
-
return new import_stdio2.StdioClientTransport({
|
|
1989
|
-
command: config.command,
|
|
1990
|
-
args: config.args,
|
|
1991
|
-
env: config.env
|
|
1992
|
-
});
|
|
1993
|
-
case "streamable-http":
|
|
1994
|
-
return new import_streamableHttp.StreamableHTTPClientTransport(
|
|
1995
|
-
new URL(config.url),
|
|
1996
|
-
config.headers ? { requestInit: { headers: config.headers } } : void 0
|
|
1997
|
-
);
|
|
1998
|
-
case "sse":
|
|
1999
|
-
return new import_sse.SSEClientTransport(
|
|
2000
|
-
new URL(config.url),
|
|
2001
|
-
config.headers ? { requestInit: { headers: config.headers } } : void 0
|
|
2002
|
-
);
|
|
2003
|
-
default: {
|
|
2004
|
-
const _exhaustive = config;
|
|
2005
|
-
return _exhaustive;
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
3198
|
// src/proxy/index.ts
|
|
2011
3199
|
var SINGLE_MCP_NAME = "__default__";
|
|
2012
3200
|
async function startProxy(command, args) {
|
|
@@ -2024,7 +3212,7 @@ async function startProxyFromConfig(options = {}) {
|
|
|
2024
3212
|
const eagerMcps = /* @__PURE__ */ new Map();
|
|
2025
3213
|
const lazyMcps = /* @__PURE__ */ new Map();
|
|
2026
3214
|
for (const [name, entry] of Object.entries(config.mcp)) {
|
|
2027
|
-
const transport = createTransport(entry);
|
|
3215
|
+
const transport = createTransport(name, entry);
|
|
2028
3216
|
if (entry.description !== void 0) {
|
|
2029
3217
|
lazyMcps.set(name, { transport, description: entry.description });
|
|
2030
3218
|
} else {
|
|
@@ -2046,7 +3234,7 @@ function buildOrchestrator(params) {
|
|
|
2046
3234
|
lazyMcps: params.lazyMcps,
|
|
2047
3235
|
namespaced: params.namespaced,
|
|
2048
3236
|
onTransportError: (mcpName2, error) => {
|
|
2049
|
-
|
|
3237
|
+
import_node_process11.default.stderr.write(
|
|
2050
3238
|
`${params.transportErrorPrefix(mcpName2)} transport error: ${error.message}
|
|
2051
3239
|
`
|
|
2052
3240
|
);
|
|
@@ -2060,19 +3248,19 @@ async function runProxy(orchestrator) {
|
|
|
2060
3248
|
if (isShuttingDown) return;
|
|
2061
3249
|
isShuttingDown = true;
|
|
2062
3250
|
orchestrator.disconnectAll().catch((error) => {
|
|
2063
|
-
|
|
3251
|
+
import_node_process11.default.stderr.write(
|
|
2064
3252
|
`dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
|
|
2065
3253
|
`
|
|
2066
3254
|
);
|
|
2067
|
-
}).finally(() =>
|
|
3255
|
+
}).finally(() => import_node_process11.default.exit(exitCode));
|
|
2068
3256
|
};
|
|
2069
3257
|
activeShutdown.shutdown = shutdown;
|
|
2070
3258
|
try {
|
|
2071
3259
|
await orchestrator.connect();
|
|
2072
3260
|
} catch (error) {
|
|
2073
|
-
|
|
3261
|
+
import_node_process11.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
2074
3262
|
`);
|
|
2075
|
-
|
|
3263
|
+
import_node_process11.default.exit(1);
|
|
2076
3264
|
return;
|
|
2077
3265
|
}
|
|
2078
3266
|
const proxyServer = new ProxyServer({
|
|
@@ -2109,10 +3297,10 @@ async function runProxy(orchestrator) {
|
|
|
2109
3297
|
onElicitInput: (params, options) => proxyServer.forwardElicitInput(params, options),
|
|
2110
3298
|
onListRoots: (params, options) => proxyServer.forwardListRoots(params, options)
|
|
2111
3299
|
});
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
3300
|
+
import_node_process11.default.on("SIGINT", () => shutdown(0));
|
|
3301
|
+
import_node_process11.default.on("SIGTERM", () => shutdown(0));
|
|
3302
|
+
import_node_process11.default.stdin.on("end", () => shutdown(0));
|
|
3303
|
+
import_node_process11.default.stdin.on("close", () => shutdown(0));
|
|
2116
3304
|
try {
|
|
2117
3305
|
await proxyServer.start();
|
|
2118
3306
|
} catch (error) {
|
|
@@ -2121,6 +3309,215 @@ async function runProxy(orchestrator) {
|
|
|
2121
3309
|
}
|
|
2122
3310
|
}
|
|
2123
3311
|
|
|
3312
|
+
// src/scaffold/init.ts
|
|
3313
|
+
var import_node_fs3 = require("fs");
|
|
3314
|
+
var import_node_path4 = require("path");
|
|
3315
|
+
var import_node_process12 = __toESM(require("process"), 1);
|
|
3316
|
+
|
|
3317
|
+
// src/scaffold/format.ts
|
|
3318
|
+
var import_node_path3 = require("path");
|
|
3319
|
+
function detectFormat(filePath) {
|
|
3320
|
+
const ext = (0, import_node_path3.extname)(filePath).toLowerCase();
|
|
3321
|
+
return ext === ".yml" || ext === ".yaml" ? "yaml" : "json";
|
|
3322
|
+
}
|
|
3323
|
+
var SCHEMA_URL = "https://dynamicmcp.tools/config.json";
|
|
3324
|
+
|
|
3325
|
+
// src/scaffold/init.ts
|
|
3326
|
+
function init(options = {}) {
|
|
3327
|
+
const cwd = options.cwd ?? import_node_process12.default.cwd();
|
|
3328
|
+
const stdout = options.write ?? ((chunk) => void import_node_process12.default.stdout.write(chunk));
|
|
3329
|
+
const fileWriter = options.fileWriter ?? ((p, c) => (0, import_node_fs3.writeFileSync)(p, c, "utf-8"));
|
|
3330
|
+
const fileExists = options.fileExists ?? ((p) => (0, import_node_fs3.existsSync)(p));
|
|
3331
|
+
const targetPath = resolveInitPath({ cwd, path: options.path, yaml: options.yaml === true });
|
|
3332
|
+
const format = detectFormat(targetPath);
|
|
3333
|
+
if (fileExists(targetPath) && options.force !== true) {
|
|
3334
|
+
throw new Error(`File already exists: ${targetPath}
|
|
3335
|
+
Use --force to overwrite.`);
|
|
3336
|
+
}
|
|
3337
|
+
const contents = format === "yaml" ? renderYamlSkeleton() : renderJsonSkeleton();
|
|
3338
|
+
fileWriter(targetPath, contents);
|
|
3339
|
+
stdout(`Wrote ${targetPath}
|
|
3340
|
+
`);
|
|
3341
|
+
stdout("\nThis config has no MCPs yet. Add one with:\n");
|
|
3342
|
+
stdout(" dynmcp add <name> --command <cmd> (stdio upstream)\n");
|
|
3343
|
+
stdout(" dynmcp add <name> --transport streamable-http --url <url> (remote HTTP upstream)\n");
|
|
3344
|
+
stdout("\nSee https://dynamicmcp.tools for full documentation.\n");
|
|
3345
|
+
}
|
|
3346
|
+
function resolveInitPath(opts) {
|
|
3347
|
+
if (opts.path !== void 0) {
|
|
3348
|
+
return (0, import_node_path4.resolve)(opts.cwd, opts.path);
|
|
3349
|
+
}
|
|
3350
|
+
return (0, import_node_path4.resolve)(opts.cwd, opts.yaml ? "mcp.yaml" : "mcp.json");
|
|
3351
|
+
}
|
|
3352
|
+
function renderJsonSkeleton() {
|
|
3353
|
+
const body = JSON.stringify({ $schema: SCHEMA_URL, mcp: {} }, null, 2);
|
|
3354
|
+
return `${body}
|
|
3355
|
+
`;
|
|
3356
|
+
}
|
|
3357
|
+
function renderYamlSkeleton() {
|
|
3358
|
+
return `# yaml-language-server: $schema=${SCHEMA_URL}
|
|
3359
|
+
|
|
3360
|
+
mcp: {}
|
|
3361
|
+
`;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
// src/scaffold/add.ts
|
|
3365
|
+
var import_node_fs4 = require("fs");
|
|
3366
|
+
var import_node_process13 = __toESM(require("process"), 1);
|
|
3367
|
+
var import_yaml2 = require("yaml");
|
|
3368
|
+
function add(options) {
|
|
3369
|
+
validateName(options.name);
|
|
3370
|
+
const stdout = options.write ?? ((chunk) => void import_node_process13.default.stdout.write(chunk));
|
|
3371
|
+
const fileReader = options.fileReader ?? ((p) => (0, import_node_fs4.readFileSync)(p, "utf-8"));
|
|
3372
|
+
const fileWriter = options.fileWriter ?? ((p, c) => (0, import_node_fs4.writeFileSync)(p, c, "utf-8"));
|
|
3373
|
+
const resolvePath = options.resolvePath ?? resolveConfigPath;
|
|
3374
|
+
let targetPath;
|
|
3375
|
+
try {
|
|
3376
|
+
targetPath = resolvePath(options.configPath);
|
|
3377
|
+
} catch (error) {
|
|
3378
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3379
|
+
throw new Error(`${message}
|
|
3380
|
+
Hint: run 'dynmcp init' to create a starter config file.`);
|
|
3381
|
+
}
|
|
3382
|
+
const raw = fileReader(targetPath);
|
|
3383
|
+
const format = detectFormat(targetPath);
|
|
3384
|
+
const entry = buildEntry(options);
|
|
3385
|
+
const parsed = transportConfigSchema.safeParse(entry);
|
|
3386
|
+
if (!parsed.success) {
|
|
3387
|
+
const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "<root>"}: ${i.message}`).join("\n");
|
|
3388
|
+
throw new Error(`Invalid MCP entry:
|
|
3389
|
+
${issues}`);
|
|
3390
|
+
}
|
|
3391
|
+
const next = format === "yaml" ? writeYaml(raw, options.name, parsed.data, options.force === true) : writeJson(raw, options.name, parsed.data, options.force === true);
|
|
3392
|
+
fileWriter(targetPath, next);
|
|
3393
|
+
stdout(`Added '${options.name}' (${options.transport}) to ${targetPath}
|
|
3394
|
+
`);
|
|
3395
|
+
}
|
|
3396
|
+
function validateName(name) {
|
|
3397
|
+
if (!MCP_NAME_PATTERN.test(name)) {
|
|
3398
|
+
throw new Error(
|
|
3399
|
+
`Invalid MCP name '${name}'. Names must match ${MCP_NAME_PATTERN.source} (lowercase letters, digits, and dashes; starting with a letter or digit).`
|
|
3400
|
+
);
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
function buildEntry(options) {
|
|
3404
|
+
if (options.transport === "stdio") {
|
|
3405
|
+
if (options.command === void 0 || options.command.length === 0) {
|
|
3406
|
+
throw new Error("--command is required for stdio transport");
|
|
3407
|
+
}
|
|
3408
|
+
const entry2 = {
|
|
3409
|
+
transport: "stdio",
|
|
3410
|
+
command: options.command
|
|
3411
|
+
};
|
|
3412
|
+
if (options.description !== void 0) entry2.description = options.description;
|
|
3413
|
+
if (options.args !== void 0 && options.args.length > 0) {
|
|
3414
|
+
entry2.args = options.args;
|
|
3415
|
+
}
|
|
3416
|
+
if (options.envVars !== void 0 && options.envVars.length > 0) {
|
|
3417
|
+
entry2.env = parseKeyValuePairs(options.envVars, "--env");
|
|
3418
|
+
}
|
|
3419
|
+
return entry2;
|
|
3420
|
+
}
|
|
3421
|
+
if (options.url === void 0 || options.url.length === 0) {
|
|
3422
|
+
throw new Error(`--url is required for ${options.transport} transport`);
|
|
3423
|
+
}
|
|
3424
|
+
const entry = {
|
|
3425
|
+
transport: options.transport,
|
|
3426
|
+
url: options.url
|
|
3427
|
+
};
|
|
3428
|
+
if (options.description !== void 0) entry.description = options.description;
|
|
3429
|
+
if (options.headers !== void 0 && options.headers.length > 0) {
|
|
3430
|
+
entry.headers = parseHeaderPairs(options.headers);
|
|
3431
|
+
}
|
|
3432
|
+
const auth2 = buildAuthBlock(options);
|
|
3433
|
+
if (auth2 !== void 0) entry.auth = auth2;
|
|
3434
|
+
return entry;
|
|
3435
|
+
}
|
|
3436
|
+
function buildAuthBlock(options) {
|
|
3437
|
+
const hasAny = options.clientId !== void 0 || options.clientSecret !== void 0 || options.scope !== void 0;
|
|
3438
|
+
if (!hasAny) return void 0;
|
|
3439
|
+
if (options.clientId === void 0) {
|
|
3440
|
+
throw new Error("--client-id is required when --client-secret or --scope is provided");
|
|
3441
|
+
}
|
|
3442
|
+
const auth2 = { client_id: options.clientId };
|
|
3443
|
+
if (options.clientSecret !== void 0) auth2.client_secret = options.clientSecret;
|
|
3444
|
+
if (options.scope !== void 0) auth2.scope = options.scope;
|
|
3445
|
+
return auth2;
|
|
3446
|
+
}
|
|
3447
|
+
function parseKeyValuePairs(pairs, flag) {
|
|
3448
|
+
const out = {};
|
|
3449
|
+
for (const pair of pairs) {
|
|
3450
|
+
const eq = pair.indexOf("=");
|
|
3451
|
+
if (eq <= 0) {
|
|
3452
|
+
throw new Error(`${flag} expects KEY=VALUE (got: ${JSON.stringify(pair)})`);
|
|
3453
|
+
}
|
|
3454
|
+
const key = pair.slice(0, eq);
|
|
3455
|
+
out[key] = pair.slice(eq + 1);
|
|
3456
|
+
}
|
|
3457
|
+
return out;
|
|
3458
|
+
}
|
|
3459
|
+
function parseHeaderPairs(pairs) {
|
|
3460
|
+
const out = {};
|
|
3461
|
+
for (const pair of pairs) {
|
|
3462
|
+
const colon = pair.indexOf(":");
|
|
3463
|
+
if (colon <= 0) {
|
|
3464
|
+
throw new Error(`--header expects "Name: Value" (got: ${JSON.stringify(pair)})`);
|
|
3465
|
+
}
|
|
3466
|
+
const key = pair.slice(0, colon).trim();
|
|
3467
|
+
const value = pair.slice(colon + 1).trim();
|
|
3468
|
+
if (key.length === 0) {
|
|
3469
|
+
throw new Error(`--header name cannot be empty (got: ${JSON.stringify(pair)})`);
|
|
3470
|
+
}
|
|
3471
|
+
out[key] = value;
|
|
3472
|
+
}
|
|
3473
|
+
return out;
|
|
3474
|
+
}
|
|
3475
|
+
function writeJson(raw, name, entry, force) {
|
|
3476
|
+
let parsed;
|
|
3477
|
+
try {
|
|
3478
|
+
parsed = JSON.parse(raw);
|
|
3479
|
+
} catch (error) {
|
|
3480
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3481
|
+
throw new Error(`Failed to parse config as JSON: ${message}`);
|
|
3482
|
+
}
|
|
3483
|
+
if (!isPlainObject(parsed)) {
|
|
3484
|
+
throw new Error("Top-level config must be a JSON object.");
|
|
3485
|
+
}
|
|
3486
|
+
let mcp = parsed.mcp;
|
|
3487
|
+
if (mcp === void 0) {
|
|
3488
|
+
mcp = {};
|
|
3489
|
+
parsed.mcp = mcp;
|
|
3490
|
+
} else if (!isPlainObject(mcp)) {
|
|
3491
|
+
throw new Error("Config field 'mcp' must be an object.");
|
|
3492
|
+
}
|
|
3493
|
+
const mcpRecord = mcp;
|
|
3494
|
+
if (mcpRecord[name] !== void 0 && !force) {
|
|
3495
|
+
throw new Error(`Entry '${name}' already exists. Use --force to overwrite.`);
|
|
3496
|
+
}
|
|
3497
|
+
mcpRecord[name] = entry;
|
|
3498
|
+
return `${JSON.stringify(parsed, null, 2)}
|
|
3499
|
+
`;
|
|
3500
|
+
}
|
|
3501
|
+
function writeYaml(raw, name, entry, force) {
|
|
3502
|
+
const doc = (0, import_yaml2.parseDocument)(raw);
|
|
3503
|
+
if (doc.errors.length > 0) {
|
|
3504
|
+
const first = doc.errors[0]?.message ?? "unknown error";
|
|
3505
|
+
throw new Error(`Failed to parse config as YAML: ${first}`);
|
|
3506
|
+
}
|
|
3507
|
+
if (!doc.has("mcp")) {
|
|
3508
|
+
doc.set("mcp", {});
|
|
3509
|
+
}
|
|
3510
|
+
const path = ["mcp", name];
|
|
3511
|
+
if (doc.hasIn(path) && !force) {
|
|
3512
|
+
throw new Error(`Entry '${name}' already exists. Use --force to overwrite.`);
|
|
3513
|
+
}
|
|
3514
|
+
doc.setIn(path, entry);
|
|
3515
|
+
return doc.toString();
|
|
3516
|
+
}
|
|
3517
|
+
function isPlainObject(value) {
|
|
3518
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3519
|
+
}
|
|
3520
|
+
|
|
2124
3521
|
// src/cli.ts
|
|
2125
3522
|
var cliBanner = import_chalk.default.bold.magentaBright(
|
|
2126
3523
|
import_figlet.default.textSync("DYNAMIC MCP", {
|
|
@@ -2131,41 +3528,162 @@ var cliBanner = import_chalk.default.bold.magentaBright(
|
|
|
2131
3528
|
);
|
|
2132
3529
|
var cli = new import_commander.Command(package_default.name).description(package_default.description).version(package_default.version).addHelpText("beforeAll", cliBanner).addHelpText(
|
|
2133
3530
|
"after",
|
|
2134
|
-
"\nExamples:\n dynmcp -- npx -y chrome-devtools-mcp@latest\n dynmcp --config ./mcp.json\n"
|
|
3531
|
+
"\nExamples:\n dynmcp -- npx -y chrome-devtools-mcp@latest\n dynmcp --config ./mcp.json\n dynmcp init\n dynmcp add filesystem --command npx --arg -y --arg @modelcontextprotocol/server-filesystem --arg /tmp\n dynmcp add github --transport streamable-http --url https://api.githubcopilot.com/mcp\n dynmcp ls\n dynmcp test github\n dynmcp login github\n dynmcp logout github\n"
|
|
2135
3532
|
).option("-c, --config <path>", "Path to config file (JSON or YAML)").option("-e, --env <path>", "Path to a .env file for environment variable interpolation").allowExcessArguments(true).passThroughOptions(true).action(async (_options, cmd) => {
|
|
2136
|
-
const separatorIndex =
|
|
3533
|
+
const separatorIndex = import_node_process14.default.argv.indexOf("--");
|
|
2137
3534
|
const configPath = cmd.opts().config;
|
|
2138
3535
|
const envFilePath = cmd.opts().env;
|
|
2139
3536
|
if (separatorIndex !== -1) {
|
|
2140
|
-
const [command, ...args] =
|
|
3537
|
+
const [command, ...args] = import_node_process14.default.argv.slice(separatorIndex + 1);
|
|
2141
3538
|
if (command === void 0) {
|
|
2142
|
-
|
|
3539
|
+
import_node_process14.default.stderr.write(
|
|
2143
3540
|
"dynmcp: no upstream command provided after --.\nUsage: dynmcp -- <command> [args...]\n"
|
|
2144
3541
|
);
|
|
2145
|
-
|
|
3542
|
+
import_node_process14.default.exit(1);
|
|
2146
3543
|
}
|
|
2147
3544
|
try {
|
|
2148
3545
|
await startProxy(command, args);
|
|
2149
3546
|
} catch (error) {
|
|
2150
|
-
|
|
3547
|
+
import_node_process14.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
2151
3548
|
`);
|
|
2152
|
-
|
|
3549
|
+
import_node_process14.default.exit(1);
|
|
2153
3550
|
}
|
|
2154
3551
|
return;
|
|
2155
3552
|
}
|
|
2156
3553
|
try {
|
|
2157
3554
|
await startProxyFromConfig({ configPath, envFilePath });
|
|
2158
3555
|
} catch (error) {
|
|
2159
|
-
|
|
3556
|
+
import_node_process14.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
3557
|
+
`);
|
|
3558
|
+
import_node_process14.default.exit(1);
|
|
3559
|
+
}
|
|
3560
|
+
});
|
|
3561
|
+
cli.command("init").description("Write a starter config file (mcp.json by default) in the current directory.").option("--path <path>", "Explicit target path (extension determines format).").option("--yaml", "Write mcp.yaml instead of mcp.json (ignored if --path is set).").option("--force", "Overwrite an existing file.").action((options) => {
|
|
3562
|
+
try {
|
|
3563
|
+
init({
|
|
3564
|
+
...options.path !== void 0 ? { path: options.path } : {},
|
|
3565
|
+
...options.yaml === true ? { yaml: true } : {},
|
|
3566
|
+
...options.force === true ? { force: true } : {}
|
|
3567
|
+
});
|
|
3568
|
+
} catch (error) {
|
|
3569
|
+
import_node_process14.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
3570
|
+
`);
|
|
3571
|
+
import_node_process14.default.exit(1);
|
|
3572
|
+
}
|
|
3573
|
+
});
|
|
3574
|
+
var collectRepeatable = (value, previous) => [...previous, value];
|
|
3575
|
+
cli.command("add <name>").description("Insert a new MCP entry into the resolved config file.").option(
|
|
3576
|
+
"-t, --transport <transport>",
|
|
3577
|
+
"Transport: stdio | streamable-http | sse (default: stdio).",
|
|
3578
|
+
"stdio"
|
|
3579
|
+
).option("-c, --config <path>", "Path to config file (otherwise auto-discovered).").option("--description <text>", "Per-entry description; presence makes the entry lazy.").option("--command <cmd>", "(stdio) Command to spawn for the upstream MCP.").option(
|
|
3580
|
+
"--arg <arg>",
|
|
3581
|
+
"(stdio) Repeatable positional argument passed after --command.",
|
|
3582
|
+
collectRepeatable,
|
|
3583
|
+
[]
|
|
3584
|
+
).option(
|
|
3585
|
+
"--env <KEY=VAL>",
|
|
3586
|
+
"(stdio) Repeatable env var for the spawned process.",
|
|
3587
|
+
collectRepeatable,
|
|
3588
|
+
[]
|
|
3589
|
+
).option("--url <url>", "(http/sse) Endpoint URL.").option(
|
|
3590
|
+
"--header <header>",
|
|
3591
|
+
'(http/sse) Repeatable "Name: Value" header.',
|
|
3592
|
+
collectRepeatable,
|
|
3593
|
+
[]
|
|
3594
|
+
).option("--client-id <id>", "(http/sse) Pre-registered OAuth client_id (skips DCR).").option("--client-secret <secret>", "(http/sse) Pre-registered OAuth client_secret.").option("--scope <scope>", "(http/sse) OAuth scope to request.").option("--force", "Overwrite an existing entry with the same name.").action(
|
|
3595
|
+
(name, options) => {
|
|
3596
|
+
try {
|
|
3597
|
+
const transport = options.transport;
|
|
3598
|
+
if (transport !== "stdio" && transport !== "streamable-http" && transport !== "sse") {
|
|
3599
|
+
throw new Error(
|
|
3600
|
+
`Invalid --transport '${options.transport}'. Must be one of: stdio, streamable-http, sse.`
|
|
3601
|
+
);
|
|
3602
|
+
}
|
|
3603
|
+
add({
|
|
3604
|
+
name,
|
|
3605
|
+
transport,
|
|
3606
|
+
...options.config !== void 0 ? { configPath: options.config } : {},
|
|
3607
|
+
...options.force === true ? { force: true } : {},
|
|
3608
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
3609
|
+
...options.command !== void 0 ? { command: options.command } : {},
|
|
3610
|
+
...options.arg.length > 0 ? { args: options.arg } : {},
|
|
3611
|
+
...options.env.length > 0 ? { envVars: options.env } : {},
|
|
3612
|
+
...options.url !== void 0 ? { url: options.url } : {},
|
|
3613
|
+
...options.header.length > 0 ? { headers: options.header } : {},
|
|
3614
|
+
...options.clientId !== void 0 ? { clientId: options.clientId } : {},
|
|
3615
|
+
...options.clientSecret !== void 0 ? { clientSecret: options.clientSecret } : {},
|
|
3616
|
+
...options.scope !== void 0 ? { scope: options.scope } : {}
|
|
3617
|
+
});
|
|
3618
|
+
} catch (error) {
|
|
3619
|
+
import_node_process14.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
3620
|
+
`);
|
|
3621
|
+
import_node_process14.default.exit(1);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
);
|
|
3625
|
+
cli.command("login <name>").description("Run the OAuth authorization-code flow for an upstream MCP and store tokens.").option("-c, --config <path>", "Path to config file (JSON or YAML)").option("-e, --env <path>", "Path to a .env file for environment variable interpolation").action(async (name, options) => {
|
|
3626
|
+
try {
|
|
3627
|
+
await login({
|
|
3628
|
+
mcpName: name,
|
|
3629
|
+
...options.config !== void 0 ? { configPath: options.config } : {},
|
|
3630
|
+
...options.env !== void 0 ? { envFilePath: options.env } : {}
|
|
3631
|
+
});
|
|
3632
|
+
} catch (error) {
|
|
3633
|
+
import_node_process14.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
3634
|
+
`);
|
|
3635
|
+
import_node_process14.default.exit(1);
|
|
3636
|
+
}
|
|
3637
|
+
});
|
|
3638
|
+
cli.command("logout <name>").description("Delete the OAuth keychain entry for an upstream MCP.").option("-c, --config <path>", "Path to config file (JSON or YAML)").option("-e, --env <path>", "Path to a .env file for environment variable interpolation").action(async (name, options) => {
|
|
3639
|
+
try {
|
|
3640
|
+
await logout({
|
|
3641
|
+
mcpName: name,
|
|
3642
|
+
...options.config !== void 0 ? { configPath: options.config } : {},
|
|
3643
|
+
...options.env !== void 0 ? { envFilePath: options.env } : {}
|
|
3644
|
+
});
|
|
3645
|
+
} catch (error) {
|
|
3646
|
+
import_node_process14.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
3647
|
+
`);
|
|
3648
|
+
import_node_process14.default.exit(1);
|
|
3649
|
+
}
|
|
3650
|
+
});
|
|
3651
|
+
cli.command("ls").description("List configured upstream MCPs with transport, mode, endpoint, and auth status.").option("-c, --config <path>", "Path to config file (JSON or YAML)").option("-e, --env <path>", "Path to a .env file for environment variable interpolation").option("--json", "Emit JSON instead of the aligned text table").action(async (options) => {
|
|
3652
|
+
try {
|
|
3653
|
+
await list({
|
|
3654
|
+
...options.config !== void 0 ? { configPath: options.config } : {},
|
|
3655
|
+
...options.env !== void 0 ? { envFilePath: options.env } : {},
|
|
3656
|
+
...options.json === true ? { json: true } : {}
|
|
3657
|
+
});
|
|
3658
|
+
} catch (error) {
|
|
3659
|
+
import_node_process14.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
2160
3660
|
`);
|
|
2161
|
-
|
|
3661
|
+
import_node_process14.default.exit(1);
|
|
2162
3662
|
}
|
|
2163
3663
|
});
|
|
3664
|
+
cli.command("test [name]").description("Probe one or all configured upstream MCPs and print their discovered catalogs.").option("-c, --config <path>", "Path to config file (JSON or YAML)").option("-e, --env <path>", "Path to a .env file for environment variable interpolation").option("--json", "Emit JSON instead of the formatted text output").option("--timeout <ms>", "Per-MCP timeout in milliseconds (default: 15000)", (v) => Number(v)).action(
|
|
3665
|
+
async (name, options) => {
|
|
3666
|
+
try {
|
|
3667
|
+
const exitCode = await test({
|
|
3668
|
+
...name !== void 0 ? { mcpName: name } : {},
|
|
3669
|
+
...options.config !== void 0 ? { configPath: options.config } : {},
|
|
3670
|
+
...options.env !== void 0 ? { envFilePath: options.env } : {},
|
|
3671
|
+
...options.json === true ? { json: true } : {},
|
|
3672
|
+
...options.timeout !== void 0 && !Number.isNaN(options.timeout) ? { timeoutMs: options.timeout } : {}
|
|
3673
|
+
});
|
|
3674
|
+
if (exitCode !== 0) import_node_process14.default.exit(exitCode);
|
|
3675
|
+
} catch (error) {
|
|
3676
|
+
import_node_process14.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
3677
|
+
`);
|
|
3678
|
+
import_node_process14.default.exit(1);
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
);
|
|
2164
3682
|
|
|
2165
3683
|
// src/index.ts
|
|
2166
|
-
var
|
|
3684
|
+
var import_node_process15 = __toESM(require("process"), 1);
|
|
2167
3685
|
async function main() {
|
|
2168
|
-
cli.parse(
|
|
3686
|
+
cli.parse(import_node_process15.default.argv);
|
|
2169
3687
|
}
|
|
2170
3688
|
main();
|
|
2171
3689
|
//# sourceMappingURL=index.cjs.map
|