dynmcp 0.4.0 → 0.5.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/README.md +29 -195
- package/dist/index.cjs +1538 -295
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1650 -407
- package/dist/index.js.map +1 -1
- package/package.json +4 -5
- package/schema/mcp-config.json +0 -138
package/dist/index.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import process12 from "process";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// package.json
|
|
8
8
|
var package_default = {
|
|
9
9
|
name: "dynmcp",
|
|
10
|
-
version: "0.
|
|
10
|
+
version: "0.5.0",
|
|
11
11
|
description: "Dynamic MCP context management tool for AI MCP-enabled agents and clients.",
|
|
12
12
|
author: "Brandon Burrus <brandon@burrus.io>",
|
|
13
13
|
license: "MIT",
|
|
14
14
|
type: "module",
|
|
15
|
-
homepage: "https://
|
|
15
|
+
homepage: "https://dynamicmcp.tools",
|
|
16
16
|
keywords: [
|
|
17
17
|
"mcp",
|
|
18
18
|
"model-context-protocol",
|
|
@@ -52,12 +52,10 @@ var package_default = {
|
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
54
|
files: [
|
|
55
|
-
"dist"
|
|
56
|
-
"schema"
|
|
55
|
+
"dist"
|
|
57
56
|
],
|
|
58
57
|
scripts: {
|
|
59
58
|
"generate:schema": "tsx scripts/generate-schema.ts",
|
|
60
|
-
prebuild: "tsx scripts/generate-schema.ts",
|
|
61
59
|
build: "tsup",
|
|
62
60
|
dev: "tsx src/index.ts",
|
|
63
61
|
typecheck: "tsc --noEmit",
|
|
@@ -71,6 +69,7 @@ var package_default = {
|
|
|
71
69
|
},
|
|
72
70
|
dependencies: {
|
|
73
71
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
72
|
+
"@napi-rs/keyring": "^1.3.0",
|
|
74
73
|
boxen: "^8.0.1",
|
|
75
74
|
chalk: "^5.6.2",
|
|
76
75
|
commander: "^14.0.3",
|
|
@@ -100,9 +99,330 @@ var package_default = {
|
|
|
100
99
|
import figlet from "figlet";
|
|
101
100
|
import chalk from "chalk";
|
|
102
101
|
|
|
103
|
-
// src/
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
// src/auth/errors.ts
|
|
103
|
+
var AuthRequiredError = class extends Error {
|
|
104
|
+
mcpName;
|
|
105
|
+
constructor(mcpName2) {
|
|
106
|
+
super(
|
|
107
|
+
`Upstream MCP "${mcpName2}" requires authorization. Run \`dynmcp login ${mcpName2}\` from your terminal, then retry.`
|
|
108
|
+
);
|
|
109
|
+
this.name = "AuthRequiredError";
|
|
110
|
+
this.mcpName = mcpName2;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
function isAuthRequiredError(error) {
|
|
114
|
+
let current = error;
|
|
115
|
+
for (let depth = 0; depth < 8 && current !== null && current !== void 0; depth += 1) {
|
|
116
|
+
if (current instanceof AuthRequiredError) return true;
|
|
117
|
+
if (current instanceof Error && current.name === "AuthRequiredError") return true;
|
|
118
|
+
if (current instanceof Error) {
|
|
119
|
+
current = current.cause;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/auth/keychain-store.ts
|
|
128
|
+
import { Entry } from "@napi-rs/keyring";
|
|
129
|
+
|
|
130
|
+
// src/auth/types.ts
|
|
131
|
+
var KEYCHAIN_BLOB_VERSION = 1;
|
|
132
|
+
|
|
133
|
+
// src/auth/keychain-store.ts
|
|
134
|
+
var KEYCHAIN_SERVICE = "dynmcp";
|
|
135
|
+
function buildKeychainAccount(mcpName2, serverUrl) {
|
|
136
|
+
const origin = new URL(serverUrl).origin;
|
|
137
|
+
return `${mcpName2}:${origin}`;
|
|
138
|
+
}
|
|
139
|
+
var KeychainStore = class {
|
|
140
|
+
constructor(mcpName2, serverUrl, service = KEYCHAIN_SERVICE) {
|
|
141
|
+
this.mcpName = mcpName2;
|
|
142
|
+
this.serverUrl = serverUrl;
|
|
143
|
+
this.entry = new Entry(service, buildKeychainAccount(mcpName2, serverUrl));
|
|
144
|
+
}
|
|
145
|
+
mcpName;
|
|
146
|
+
serverUrl;
|
|
147
|
+
entry;
|
|
148
|
+
/**
|
|
149
|
+
* Returns the parsed blob or `undefined` if no entry exists. Entries written under
|
|
150
|
+
* a different {@link KeychainBlob.version} are treated as absent — the caller must
|
|
151
|
+
* re-authenticate. Malformed JSON also returns `undefined` (corrupt entries are not
|
|
152
|
+
* surfaced as errors; recovery is the same: re-auth).
|
|
153
|
+
*/
|
|
154
|
+
get() {
|
|
155
|
+
const raw = this.entry.getPassword();
|
|
156
|
+
if (raw === null) return void 0;
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(raw);
|
|
160
|
+
} catch {
|
|
161
|
+
return void 0;
|
|
162
|
+
}
|
|
163
|
+
if (!isCurrentVersionBlob(parsed)) {
|
|
164
|
+
return void 0;
|
|
165
|
+
}
|
|
166
|
+
return parsed;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Persists the blob atomically. Caller must construct a complete {@link
|
|
170
|
+
* KeychainBlob} — partial updates are not supported. To mutate, call {@link get},
|
|
171
|
+
* spread, and pass the result back to {@link set}.
|
|
172
|
+
*/
|
|
173
|
+
set(blob) {
|
|
174
|
+
const stamped = { ...blob, version: KEYCHAIN_BLOB_VERSION };
|
|
175
|
+
this.entry.setPassword(JSON.stringify(stamped));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Deletes the entry. Returns `true` if an entry was present and removed, `false`
|
|
179
|
+
* if there was nothing to delete. Idempotent: callers should treat both outcomes
|
|
180
|
+
* as success (a no-op delete is not an error).
|
|
181
|
+
*/
|
|
182
|
+
delete() {
|
|
183
|
+
return this.entry.deletePassword();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
function isCurrentVersionBlob(value) {
|
|
187
|
+
return typeof value === "object" && value !== null && "version" in value && value.version === KEYCHAIN_BLOB_VERSION;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/auth/oauth-provider.ts
|
|
191
|
+
import { randomBytes } from "crypto";
|
|
192
|
+
var CLIENT_NAME = "dynmcp";
|
|
193
|
+
var SOFTWARE_ID = "dynmcp";
|
|
194
|
+
var REFRESH_SLACK_SECONDS = 30;
|
|
195
|
+
var BaseOAuthProvider = class {
|
|
196
|
+
constructor(mcpName2, keychain, configAuth) {
|
|
197
|
+
this.mcpName = mcpName2;
|
|
198
|
+
this.keychain = keychain;
|
|
199
|
+
this.configAuth = configAuth;
|
|
200
|
+
}
|
|
201
|
+
mcpName;
|
|
202
|
+
keychain;
|
|
203
|
+
configAuth;
|
|
204
|
+
clientInformation() {
|
|
205
|
+
if (this.configAuth !== void 0) {
|
|
206
|
+
const info2 = { client_id: this.configAuth.client_id };
|
|
207
|
+
if (this.configAuth.client_secret !== void 0) {
|
|
208
|
+
info2.client_secret = this.configAuth.client_secret;
|
|
209
|
+
}
|
|
210
|
+
return info2;
|
|
211
|
+
}
|
|
212
|
+
const blob = this.keychain.get();
|
|
213
|
+
if (blob?.dcr === void 0) return void 0;
|
|
214
|
+
const info = { client_id: blob.dcr.client_id };
|
|
215
|
+
if (blob.dcr.client_secret !== void 0) {
|
|
216
|
+
info.client_secret = blob.dcr.client_secret;
|
|
217
|
+
}
|
|
218
|
+
return info;
|
|
219
|
+
}
|
|
220
|
+
tokens() {
|
|
221
|
+
const blob = this.keychain.get();
|
|
222
|
+
if (blob === void 0) return void 0;
|
|
223
|
+
const remaining = Math.max(
|
|
224
|
+
0,
|
|
225
|
+
blob.expires_at - Math.floor(Date.now() / 1e3) - REFRESH_SLACK_SECONDS
|
|
226
|
+
);
|
|
227
|
+
const tokens = {
|
|
228
|
+
access_token: blob.access_token,
|
|
229
|
+
token_type: blob.token_type,
|
|
230
|
+
expires_in: remaining
|
|
231
|
+
};
|
|
232
|
+
if (blob.refresh_token !== void 0) tokens.refresh_token = blob.refresh_token;
|
|
233
|
+
if (blob.scope_granted !== void 0) tokens.scope = blob.scope_granted;
|
|
234
|
+
return tokens;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Builds the {@link OAuthDiscoveryState} the SDK can use to skip rediscovery,
|
|
238
|
+
* reconstructed from the cached keychain blob. Returns `undefined` when there is
|
|
239
|
+
* no cached blob (e.g. fresh login flow before saveTokens fires).
|
|
240
|
+
*/
|
|
241
|
+
buildDiscoveryStateFromBlob(blob) {
|
|
242
|
+
if (blob === void 0) return void 0;
|
|
243
|
+
return {
|
|
244
|
+
authorizationServerUrl: blob.authorization_server.issuer,
|
|
245
|
+
authorizationServerMetadata: {
|
|
246
|
+
issuer: blob.authorization_server.issuer,
|
|
247
|
+
authorization_endpoint: blob.authorization_server.authorization_endpoint,
|
|
248
|
+
token_endpoint: blob.authorization_server.token_endpoint,
|
|
249
|
+
...blob.authorization_server.registration_endpoint !== void 0 ? { registration_endpoint: blob.authorization_server.registration_endpoint } : {},
|
|
250
|
+
response_types_supported: ["code"]
|
|
251
|
+
},
|
|
252
|
+
resourceMetadata: {
|
|
253
|
+
resource: blob.resource_metadata.resource,
|
|
254
|
+
authorization_servers: blob.resource_metadata.authorization_servers
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
var ProxyOAuthProvider = class extends BaseOAuthProvider {
|
|
260
|
+
get redirectUrl() {
|
|
261
|
+
return void 0;
|
|
262
|
+
}
|
|
263
|
+
get clientMetadata() {
|
|
264
|
+
return {
|
|
265
|
+
client_name: CLIENT_NAME,
|
|
266
|
+
software_id: SOFTWARE_ID,
|
|
267
|
+
redirect_uris: [],
|
|
268
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
269
|
+
response_types: ["code"],
|
|
270
|
+
token_endpoint_auth_method: this.configAuth?.client_secret !== void 0 ? "client_secret_basic" : "none",
|
|
271
|
+
...this.configAuth?.scope !== void 0 ? { scope: this.configAuth.scope } : {}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
saveTokens(tokens) {
|
|
275
|
+
const existing = this.keychain.get();
|
|
276
|
+
if (existing === void 0) {
|
|
277
|
+
throw new AuthRequiredError(this.mcpName);
|
|
278
|
+
}
|
|
279
|
+
const expiresAt = tokens.expires_in !== void 0 ? Math.floor(Date.now() / 1e3) + tokens.expires_in : existing.expires_at;
|
|
280
|
+
const updated = {
|
|
281
|
+
...existing,
|
|
282
|
+
access_token: tokens.access_token,
|
|
283
|
+
token_type: tokens.token_type ?? "Bearer",
|
|
284
|
+
expires_at: expiresAt,
|
|
285
|
+
refresh_token: tokens.refresh_token ?? existing.refresh_token,
|
|
286
|
+
...tokens.scope !== void 0 ? { scope_granted: tokens.scope } : {}
|
|
287
|
+
};
|
|
288
|
+
this.keychain.set(updated);
|
|
289
|
+
}
|
|
290
|
+
redirectToAuthorization(_url) {
|
|
291
|
+
throw new AuthRequiredError(this.mcpName);
|
|
292
|
+
}
|
|
293
|
+
saveCodeVerifier(_verifier) {
|
|
294
|
+
throw new AuthRequiredError(this.mcpName);
|
|
295
|
+
}
|
|
296
|
+
codeVerifier() {
|
|
297
|
+
throw new AuthRequiredError(this.mcpName);
|
|
298
|
+
}
|
|
299
|
+
discoveryState() {
|
|
300
|
+
return this.buildDiscoveryStateFromBlob(this.keychain.get());
|
|
301
|
+
}
|
|
302
|
+
invalidateCredentials(_scope) {
|
|
303
|
+
this.keychain.delete();
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
var LoginOAuthProvider = class extends BaseOAuthProvider {
|
|
307
|
+
redirectUriString;
|
|
308
|
+
pending = {};
|
|
309
|
+
callbacks;
|
|
310
|
+
constructor(opts) {
|
|
311
|
+
super(opts.mcpName, opts.keychain, opts.configAuth);
|
|
312
|
+
this.redirectUriString = opts.redirectUri;
|
|
313
|
+
this.callbacks = opts.callbacks;
|
|
314
|
+
}
|
|
315
|
+
get redirectUrl() {
|
|
316
|
+
return this.redirectUriString;
|
|
317
|
+
}
|
|
318
|
+
get clientMetadata() {
|
|
319
|
+
return {
|
|
320
|
+
client_name: CLIENT_NAME,
|
|
321
|
+
software_id: SOFTWARE_ID,
|
|
322
|
+
redirect_uris: [this.redirectUriString],
|
|
323
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
324
|
+
response_types: ["code"],
|
|
325
|
+
token_endpoint_auth_method: this.configAuth?.client_secret !== void 0 ? "client_secret_basic" : "none",
|
|
326
|
+
...this.configAuth?.scope !== void 0 ? { scope: this.configAuth.scope } : {}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
state() {
|
|
330
|
+
if (this.pending.state !== void 0) return this.pending.state;
|
|
331
|
+
const generated = randomBytes(32).toString("base64url");
|
|
332
|
+
this.pending.state = generated;
|
|
333
|
+
return generated;
|
|
334
|
+
}
|
|
335
|
+
/** The state value generated for this flow, for the callback handler to verify. */
|
|
336
|
+
get currentState() {
|
|
337
|
+
return this.pending.state;
|
|
338
|
+
}
|
|
339
|
+
clientInformation() {
|
|
340
|
+
if (this.pending.dcr !== void 0) {
|
|
341
|
+
const info = { client_id: this.pending.dcr.client_id };
|
|
342
|
+
if (this.pending.dcr.client_secret !== void 0) {
|
|
343
|
+
info.client_secret = this.pending.dcr.client_secret;
|
|
344
|
+
}
|
|
345
|
+
return info;
|
|
346
|
+
}
|
|
347
|
+
return super.clientInformation();
|
|
348
|
+
}
|
|
349
|
+
saveClientInformation(info) {
|
|
350
|
+
this.pending.dcr = info;
|
|
351
|
+
}
|
|
352
|
+
saveTokens(tokens) {
|
|
353
|
+
const discovery = this.pending.discovery ?? this.buildDiscoveryStateFromBlob(this.keychain.get());
|
|
354
|
+
if (discovery === void 0) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`Cannot persist tokens for "${this.mcpName}": no discovery state captured during the flow.`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
if (discovery.authorizationServerMetadata === void 0) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`Cannot persist tokens for "${this.mcpName}": authorization server metadata not available.`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
if (discovery.resourceMetadata === void 0) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Cannot persist tokens for "${this.mcpName}": protected resource metadata not available.`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
const expiresAt = tokens.expires_in !== void 0 ? Math.floor(Date.now() / 1e3) + tokens.expires_in : Math.floor(Date.now() / 1e3) + 3600;
|
|
370
|
+
const authorizationServer = {
|
|
371
|
+
issuer: discovery.authorizationServerMetadata.issuer ?? discovery.authorizationServerUrl,
|
|
372
|
+
authorization_endpoint: discovery.authorizationServerMetadata.authorization_endpoint,
|
|
373
|
+
token_endpoint: discovery.authorizationServerMetadata.token_endpoint,
|
|
374
|
+
...discovery.authorizationServerMetadata.registration_endpoint !== void 0 ? { registration_endpoint: discovery.authorizationServerMetadata.registration_endpoint } : {}
|
|
375
|
+
};
|
|
376
|
+
const resourceMetadata = {
|
|
377
|
+
resource: discovery.resourceMetadata.resource,
|
|
378
|
+
authorization_servers: discovery.resourceMetadata.authorization_servers ?? []
|
|
379
|
+
};
|
|
380
|
+
const blob = {
|
|
381
|
+
version: KEYCHAIN_BLOB_VERSION,
|
|
382
|
+
access_token: tokens.access_token,
|
|
383
|
+
token_type: tokens.token_type ?? "Bearer",
|
|
384
|
+
expires_at: expiresAt,
|
|
385
|
+
...tokens.refresh_token !== void 0 ? { refresh_token: tokens.refresh_token } : {},
|
|
386
|
+
...tokens.scope !== void 0 ? { scope_granted: tokens.scope } : {},
|
|
387
|
+
authorization_server: authorizationServer,
|
|
388
|
+
resource_metadata: resourceMetadata,
|
|
389
|
+
...this.pending.dcr !== void 0 ? {
|
|
390
|
+
dcr: {
|
|
391
|
+
client_id: this.pending.dcr.client_id,
|
|
392
|
+
...this.pending.dcr.client_secret !== void 0 ? { client_secret: this.pending.dcr.client_secret } : {}
|
|
393
|
+
}
|
|
394
|
+
} : {}
|
|
395
|
+
};
|
|
396
|
+
this.keychain.set(blob);
|
|
397
|
+
}
|
|
398
|
+
async redirectToAuthorization(url) {
|
|
399
|
+
await this.callbacks.onAuthorizationUrl(url);
|
|
400
|
+
}
|
|
401
|
+
saveCodeVerifier(verifier) {
|
|
402
|
+
this.pending.codeVerifier = verifier;
|
|
403
|
+
}
|
|
404
|
+
codeVerifier() {
|
|
405
|
+
if (this.pending.codeVerifier === void 0) {
|
|
406
|
+
throw new Error("Code verifier requested before it was saved.");
|
|
407
|
+
}
|
|
408
|
+
return this.pending.codeVerifier;
|
|
409
|
+
}
|
|
410
|
+
saveDiscoveryState(state) {
|
|
411
|
+
this.pending.discovery = state;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Force a fresh RFC 9728 + RFC 8414 discovery for every login flow. We intentionally
|
|
415
|
+
* do NOT pre-seed from the keychain on login — if endpoints changed since the last
|
|
416
|
+
* login, we want to pick them up now and persist the new snapshot.
|
|
417
|
+
*/
|
|
418
|
+
discoveryState() {
|
|
419
|
+
return void 0;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// src/auth/login.ts
|
|
424
|
+
import process4 from "process";
|
|
425
|
+
import { auth, extractWWWAuthenticateParams } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
106
426
|
|
|
107
427
|
// src/config/schema.ts
|
|
108
428
|
import { z } from "zod";
|
|
@@ -124,17 +444,26 @@ var stdioTransport = z.object({
|
|
|
124
444
|
var httpUrl = z.string().url().refine((u) => u.startsWith("http://") || u.startsWith("https://"), {
|
|
125
445
|
message: "URL must use http:// or https:// scheme"
|
|
126
446
|
});
|
|
447
|
+
var authConfig = z.object({
|
|
448
|
+
client_id: z.string().min(1, { message: "auth.client_id must be a non-empty string" }).refine((value) => value.trim().length > 0, {
|
|
449
|
+
message: "auth.client_id must not be whitespace-only"
|
|
450
|
+
}),
|
|
451
|
+
client_secret: z.string().min(1).optional(),
|
|
452
|
+
scope: z.string().min(1).optional()
|
|
453
|
+
}).strict().optional();
|
|
127
454
|
var streamableHttpTransport = z.object({
|
|
128
455
|
transport: z.literal("streamable-http"),
|
|
129
456
|
description,
|
|
130
457
|
url: httpUrl,
|
|
131
|
-
headers: z.record(z.string(), z.string()).optional()
|
|
458
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
459
|
+
auth: authConfig
|
|
132
460
|
}).strict();
|
|
133
461
|
var sseTransport = z.object({
|
|
134
462
|
transport: z.literal("sse"),
|
|
135
463
|
description,
|
|
136
464
|
url: httpUrl,
|
|
137
|
-
headers: z.record(z.string(), z.string()).optional()
|
|
465
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
466
|
+
auth: authConfig
|
|
138
467
|
}).strict();
|
|
139
468
|
var transportConfig = z.discriminatedUnion("transport", [
|
|
140
469
|
stdioTransport,
|
|
@@ -210,9 +539,9 @@ function filterDefined(env) {
|
|
|
210
539
|
var TOP_LEVEL_PASSTHROUGH_KEYS = /* @__PURE__ */ new Set(["$schema", "env"]);
|
|
211
540
|
var MissingEnvVarsError = class extends Error {
|
|
212
541
|
constructor(missingVars) {
|
|
213
|
-
const
|
|
542
|
+
const list2 = missingVars.join(", ");
|
|
214
543
|
const plural = missingVars.length === 1 ? "" : "s";
|
|
215
|
-
super(`Missing required environment variable${plural}: ${
|
|
544
|
+
super(`Missing required environment variable${plural}: ${list2}`);
|
|
216
545
|
this.missingVars = missingVars;
|
|
217
546
|
this.name = "MissingEnvVarsError";
|
|
218
547
|
}
|
|
@@ -341,37 +670,1133 @@ function loadConfig(options = {}) {
|
|
|
341
670
|
const message = parseError instanceof Error ? parseError.message : String(parseError);
|
|
342
671
|
throw new Error(`Failed to parse config file (${resolvedPath}): ${message}`);
|
|
343
672
|
}
|
|
344
|
-
const envMode = readEnvMode(content);
|
|
345
|
-
const loadedEnv = loadEnv({ mode: envMode, envFilePath });
|
|
346
|
-
const interpolated = loadedEnv.interpolationEnabled ? interpolateConfig(content, loadedEnv.variables) : content;
|
|
347
|
-
const result = mcpConfigSchema.safeParse(interpolated);
|
|
348
|
-
if (!result.success) {
|
|
349
|
-
const formatted = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
|
|
350
|
-
throw new Error(`Invalid config file (${resolvedPath}):
|
|
351
|
-
${formatted}`);
|
|
673
|
+
const envMode = readEnvMode(content);
|
|
674
|
+
const loadedEnv = loadEnv({ mode: envMode, envFilePath });
|
|
675
|
+
const interpolated = loadedEnv.interpolationEnabled ? interpolateConfig(content, loadedEnv.variables) : content;
|
|
676
|
+
const result = mcpConfigSchema.safeParse(interpolated);
|
|
677
|
+
if (!result.success) {
|
|
678
|
+
const formatted = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
|
|
679
|
+
throw new Error(`Invalid config file (${resolvedPath}):
|
|
680
|
+
${formatted}`);
|
|
681
|
+
}
|
|
682
|
+
return result.data;
|
|
683
|
+
}
|
|
684
|
+
function readEnvMode(content) {
|
|
685
|
+
if (content === null || typeof content !== "object" || Array.isArray(content)) {
|
|
686
|
+
return DEFAULT_ENV_MODE;
|
|
687
|
+
}
|
|
688
|
+
const value = content.env;
|
|
689
|
+
if (value === void 0) return DEFAULT_ENV_MODE;
|
|
690
|
+
if (typeof value === "string" && VALID_ENV_MODES.includes(value)) {
|
|
691
|
+
return value;
|
|
692
|
+
}
|
|
693
|
+
return DEFAULT_ENV_MODE;
|
|
694
|
+
}
|
|
695
|
+
function isYamlFile(filePath) {
|
|
696
|
+
return filePath.endsWith(".yml") || filePath.endsWith(".yaml");
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/config/json-schema.ts
|
|
700
|
+
import { z as z2 } from "zod";
|
|
701
|
+
|
|
702
|
+
// src/auth/browser.ts
|
|
703
|
+
import { spawn } from "child_process";
|
|
704
|
+
import process3 from "process";
|
|
705
|
+
async function openUrl(url) {
|
|
706
|
+
const { command, args } = openerForPlatform(url);
|
|
707
|
+
return new Promise((resolve3, reject) => {
|
|
708
|
+
const child = spawn(command, args, {
|
|
709
|
+
stdio: "ignore",
|
|
710
|
+
detached: true
|
|
711
|
+
});
|
|
712
|
+
child.once("error", reject);
|
|
713
|
+
child.once("spawn", () => {
|
|
714
|
+
child.unref();
|
|
715
|
+
resolve3();
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
function openerForPlatform(url) {
|
|
720
|
+
switch (process3.platform) {
|
|
721
|
+
case "darwin":
|
|
722
|
+
return { command: "open", args: [url] };
|
|
723
|
+
case "win32":
|
|
724
|
+
return { command: "cmd", args: ["/c", "start", '""', url] };
|
|
725
|
+
default:
|
|
726
|
+
return { command: "xdg-open", args: [url] };
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/auth/callback-server.ts
|
|
731
|
+
import { createServer } from "http";
|
|
732
|
+
var CallbackTimeoutError = class extends Error {
|
|
733
|
+
constructor(timeoutMs) {
|
|
734
|
+
super(`Timed out after ${timeoutMs}ms waiting for the OAuth callback.`);
|
|
735
|
+
this.name = "CallbackTimeoutError";
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
var CallbackOAuthError = class extends Error {
|
|
739
|
+
constructor(oauthError, oauthErrorDescription) {
|
|
740
|
+
super(
|
|
741
|
+
oauthErrorDescription ? `OAuth error from authorization server: ${oauthError} \u2014 ${oauthErrorDescription}` : `OAuth error from authorization server: ${oauthError}`
|
|
742
|
+
);
|
|
743
|
+
this.oauthError = oauthError;
|
|
744
|
+
this.oauthErrorDescription = oauthErrorDescription;
|
|
745
|
+
this.name = "CallbackOAuthError";
|
|
746
|
+
}
|
|
747
|
+
oauthError;
|
|
748
|
+
oauthErrorDescription;
|
|
749
|
+
};
|
|
750
|
+
var SUCCESS_HTML = `<!DOCTYPE html>
|
|
751
|
+
<html lang="en">
|
|
752
|
+
<head>
|
|
753
|
+
<meta charset="utf-8" />
|
|
754
|
+
<title>dynmcp \u2014 authorization complete</title>
|
|
755
|
+
<style>
|
|
756
|
+
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 4rem; max-width: 36rem; margin: 0 auto; color: #222; }
|
|
757
|
+
code { background: #f4f4f4; padding: 0.1rem 0.3rem; border-radius: 3px; }
|
|
758
|
+
</style>
|
|
759
|
+
</head>
|
|
760
|
+
<body>
|
|
761
|
+
<h1>Authorization complete</h1>
|
|
762
|
+
<p>You may close this tab and return to your terminal.</p>
|
|
763
|
+
<p><small>Issued by <code>dynmcp</code>.</small></p>
|
|
764
|
+
</body>
|
|
765
|
+
</html>
|
|
766
|
+
`;
|
|
767
|
+
var ERROR_HTML_PREFIX = `<!DOCTYPE html>
|
|
768
|
+
<html lang="en">
|
|
769
|
+
<head><meta charset="utf-8" /><title>dynmcp \u2014 authorization failed</title></head>
|
|
770
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 4rem; max-width: 36rem; margin: 0 auto;">
|
|
771
|
+
<h1>Authorization failed</h1>
|
|
772
|
+
<p>`;
|
|
773
|
+
var ERROR_HTML_SUFFIX = `</p>
|
|
774
|
+
<p>Return to your terminal for details.</p>
|
|
775
|
+
</body>
|
|
776
|
+
</html>
|
|
777
|
+
`;
|
|
778
|
+
var CallbackServer = class _CallbackServer {
|
|
779
|
+
server = null;
|
|
780
|
+
boundPort = null;
|
|
781
|
+
pending = null;
|
|
782
|
+
/** The redirect path served. Must match the redirect URI registered with the OAuth client. */
|
|
783
|
+
static CALLBACK_PATH = "/callback";
|
|
784
|
+
/** Begins listening on `127.0.0.1` at an OS-assigned port. */
|
|
785
|
+
async start() {
|
|
786
|
+
if (this.server !== null) {
|
|
787
|
+
throw new Error("CallbackServer is already started.");
|
|
788
|
+
}
|
|
789
|
+
const server = createServer((req, res) => this.handleRequest(req, res));
|
|
790
|
+
await new Promise((resolve3, reject) => {
|
|
791
|
+
const onError = (err) => {
|
|
792
|
+
server.removeListener("listening", onListening);
|
|
793
|
+
reject(err);
|
|
794
|
+
};
|
|
795
|
+
const onListening = () => {
|
|
796
|
+
server.removeListener("error", onError);
|
|
797
|
+
resolve3();
|
|
798
|
+
};
|
|
799
|
+
server.once("error", onError);
|
|
800
|
+
server.once("listening", onListening);
|
|
801
|
+
server.listen({ port: 0, host: "127.0.0.1" });
|
|
802
|
+
});
|
|
803
|
+
const address = server.address();
|
|
804
|
+
if (address === null || typeof address === "string") {
|
|
805
|
+
server.close();
|
|
806
|
+
throw new Error("Failed to determine bound port for callback server.");
|
|
807
|
+
}
|
|
808
|
+
this.boundPort = address.port;
|
|
809
|
+
this.server = server;
|
|
810
|
+
}
|
|
811
|
+
/** The port the OS assigned. Available after {@link start} resolves. */
|
|
812
|
+
get port() {
|
|
813
|
+
if (this.boundPort === null) {
|
|
814
|
+
throw new Error("CallbackServer is not started.");
|
|
815
|
+
}
|
|
816
|
+
return this.boundPort;
|
|
817
|
+
}
|
|
818
|
+
/** The full redirect URI to register with the authorization server. */
|
|
819
|
+
get redirectUri() {
|
|
820
|
+
return `http://127.0.0.1:${this.port}${_CallbackServer.CALLBACK_PATH}`;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Resolves with the captured `code` and `state` once a valid callback is received,
|
|
824
|
+
* or rejects with {@link CallbackTimeoutError} / {@link CallbackOAuthError} on the
|
|
825
|
+
* documented failure paths. Only one callback is accepted; subsequent requests to
|
|
826
|
+
* `/callback` after the first valid hit return `400`.
|
|
827
|
+
*/
|
|
828
|
+
awaitCallback(timeoutMs) {
|
|
829
|
+
if (this.server === null) {
|
|
830
|
+
return Promise.reject(new Error("CallbackServer is not started."));
|
|
831
|
+
}
|
|
832
|
+
if (this.pending !== null) {
|
|
833
|
+
return Promise.reject(new Error("awaitCallback already in progress."));
|
|
834
|
+
}
|
|
835
|
+
return new Promise((resolve3, reject) => {
|
|
836
|
+
const timer = setTimeout(() => {
|
|
837
|
+
this.pending = null;
|
|
838
|
+
reject(new CallbackTimeoutError(timeoutMs));
|
|
839
|
+
}, timeoutMs);
|
|
840
|
+
this.pending = { resolve: resolve3, reject, timer };
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
/** Closes the listening socket. Safe to call multiple times. */
|
|
844
|
+
async stop() {
|
|
845
|
+
if (this.pending !== null) {
|
|
846
|
+
clearTimeout(this.pending.timer);
|
|
847
|
+
this.pending = null;
|
|
848
|
+
}
|
|
849
|
+
if (this.server === null) return;
|
|
850
|
+
await new Promise((resolve3) => {
|
|
851
|
+
this.server.close(() => resolve3());
|
|
852
|
+
});
|
|
853
|
+
this.server = null;
|
|
854
|
+
this.boundPort = null;
|
|
855
|
+
}
|
|
856
|
+
handleRequest(req, res) {
|
|
857
|
+
if (req.method !== "GET") {
|
|
858
|
+
res.writeHead(405, { "content-type": "text/plain", allow: "GET" });
|
|
859
|
+
res.end("Method Not Allowed");
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${this.boundPort ?? 0}`);
|
|
863
|
+
if (url.pathname !== _CallbackServer.CALLBACK_PATH) {
|
|
864
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
865
|
+
res.end("Not Found");
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (this.pending === null) {
|
|
869
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
870
|
+
res.end("No callback expected.");
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const oauthError = url.searchParams.get("error");
|
|
874
|
+
if (oauthError !== null) {
|
|
875
|
+
const errorDescription = url.searchParams.get("error_description") ?? void 0;
|
|
876
|
+
this.respondError(res, oauthError, errorDescription);
|
|
877
|
+
const { reject, timer: timer2 } = this.pending;
|
|
878
|
+
clearTimeout(timer2);
|
|
879
|
+
this.pending = null;
|
|
880
|
+
reject(new CallbackOAuthError(oauthError, errorDescription));
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const code = url.searchParams.get("code");
|
|
884
|
+
const state = url.searchParams.get("state");
|
|
885
|
+
if (code === null || state === null) {
|
|
886
|
+
this.respondError(res, "invalid_callback", "Missing code or state parameter.");
|
|
887
|
+
const { reject, timer: timer2 } = this.pending;
|
|
888
|
+
clearTimeout(timer2);
|
|
889
|
+
this.pending = null;
|
|
890
|
+
reject(new Error("Callback missing code or state parameter."));
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
894
|
+
res.end(SUCCESS_HTML);
|
|
895
|
+
const { resolve: resolve3, timer } = this.pending;
|
|
896
|
+
clearTimeout(timer);
|
|
897
|
+
this.pending = null;
|
|
898
|
+
resolve3({ code, state });
|
|
899
|
+
}
|
|
900
|
+
respondError(res, errorCode, description2) {
|
|
901
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
902
|
+
res.end(
|
|
903
|
+
ERROR_HTML_PREFIX + escapeHtml(description2 ?? `OAuth error: ${errorCode}`) + ERROR_HTML_SUFFIX
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
function escapeHtml(value) {
|
|
908
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/auth/login.ts
|
|
912
|
+
var CALLBACK_TIMEOUT_MS = 6e4;
|
|
913
|
+
async function login(options) {
|
|
914
|
+
const config = loadConfig({
|
|
915
|
+
configPath: options.configPath,
|
|
916
|
+
envFilePath: options.envFilePath
|
|
917
|
+
});
|
|
918
|
+
const entry = resolveOAuthCapableEntry(config, options.mcpName);
|
|
919
|
+
const writeStatus = options.writeStatus ?? defaultStatusWriter;
|
|
920
|
+
const openInBrowser = options.openInBrowser ?? openUrl;
|
|
921
|
+
writeStatus(`Probing ${entry.url} for OAuth challenge...
|
|
922
|
+
`);
|
|
923
|
+
const resourceMetadataUrl = await probeFor401ResourceMetadata(entry.url);
|
|
924
|
+
if (resourceMetadataUrl === void 0) {
|
|
925
|
+
throw new Error(
|
|
926
|
+
`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.`
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
const keychain = new KeychainStore(options.mcpName, entry.url);
|
|
930
|
+
const callbackServer = new CallbackServer();
|
|
931
|
+
await callbackServer.start();
|
|
932
|
+
writeStatus(`Callback server listening on ${callbackServer.redirectUri}
|
|
933
|
+
`);
|
|
934
|
+
try {
|
|
935
|
+
const provider = new LoginOAuthProvider({
|
|
936
|
+
mcpName: options.mcpName,
|
|
937
|
+
keychain,
|
|
938
|
+
configAuth: configAuthFromEntry(entry),
|
|
939
|
+
redirectUri: callbackServer.redirectUri,
|
|
940
|
+
callbacks: {
|
|
941
|
+
onAuthorizationUrl: async (url) => {
|
|
942
|
+
writeStatus(`Opening browser for authorization: ${url.toString()}
|
|
943
|
+
`);
|
|
944
|
+
try {
|
|
945
|
+
await openInBrowser(url.toString());
|
|
946
|
+
} catch (error) {
|
|
947
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
948
|
+
writeStatus(
|
|
949
|
+
`Failed to launch browser (${reason}). Open this URL manually:
|
|
950
|
+
${url.toString()}
|
|
951
|
+
`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
const firstResult = await auth(provider, {
|
|
958
|
+
serverUrl: entry.url,
|
|
959
|
+
resourceMetadataUrl,
|
|
960
|
+
...entry.auth?.scope !== void 0 ? { scope: entry.auth.scope } : {}
|
|
961
|
+
});
|
|
962
|
+
if (firstResult === "AUTHORIZED") {
|
|
963
|
+
writeStatus(`Already authorized for "${options.mcpName}"; no changes made.
|
|
964
|
+
`);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
writeStatus(`Waiting for browser callback (timeout ${CALLBACK_TIMEOUT_MS / 1e3}s)...
|
|
968
|
+
`);
|
|
969
|
+
const { code, state: receivedState } = await callbackServer.awaitCallback(CALLBACK_TIMEOUT_MS);
|
|
970
|
+
const expectedState = provider.currentState;
|
|
971
|
+
if (expectedState === void 0 || receivedState !== expectedState) {
|
|
972
|
+
throw new Error(
|
|
973
|
+
"OAuth state mismatch on callback. Possible CSRF attempt or stale browser tab; not exchanging the authorization code."
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
const secondResult = await auth(provider, {
|
|
977
|
+
serverUrl: entry.url,
|
|
978
|
+
authorizationCode: code,
|
|
979
|
+
resourceMetadataUrl,
|
|
980
|
+
...entry.auth?.scope !== void 0 ? { scope: entry.auth.scope } : {}
|
|
981
|
+
});
|
|
982
|
+
if (secondResult !== "AUTHORIZED") {
|
|
983
|
+
throw new Error(`Token exchange did not return AUTHORIZED (got ${secondResult}).`);
|
|
984
|
+
}
|
|
985
|
+
writeStatus(`Successfully authenticated "${options.mcpName}".
|
|
986
|
+
`);
|
|
987
|
+
} finally {
|
|
988
|
+
await callbackServer.stop();
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
function resolveOAuthCapableEntry(config, mcpName2) {
|
|
992
|
+
const entry = config.mcp[mcpName2];
|
|
993
|
+
if (entry === void 0) {
|
|
994
|
+
const available = Object.keys(config.mcp).sort().join(", ");
|
|
995
|
+
throw new Error(`Unknown MCP "${mcpName2}". Configured MCPs: ${available || "(none)"}.`);
|
|
996
|
+
}
|
|
997
|
+
if (entry.transport !== "streamable-http" && entry.transport !== "sse") {
|
|
998
|
+
throw new Error(
|
|
999
|
+
`MCP "${mcpName2}" uses the "${entry.transport}" transport; OAuth is only supported for streamable-http and sse upstreams.`
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
return entry;
|
|
1003
|
+
}
|
|
1004
|
+
function configAuthFromEntry(entry) {
|
|
1005
|
+
if (entry.auth === void 0) return void 0;
|
|
1006
|
+
const overrides = { client_id: entry.auth.client_id };
|
|
1007
|
+
if (entry.auth.client_secret !== void 0) overrides.client_secret = entry.auth.client_secret;
|
|
1008
|
+
if (entry.auth.scope !== void 0) overrides.scope = entry.auth.scope;
|
|
1009
|
+
return overrides;
|
|
1010
|
+
}
|
|
1011
|
+
async function probeFor401ResourceMetadata(serverUrl) {
|
|
1012
|
+
let response;
|
|
1013
|
+
try {
|
|
1014
|
+
response = await fetch(serverUrl, { method: "GET", redirect: "manual" });
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1017
|
+
throw new Error(`Failed to reach ${serverUrl}: ${reason}`);
|
|
1018
|
+
}
|
|
1019
|
+
if (response.status !== 401) {
|
|
1020
|
+
return void 0;
|
|
1021
|
+
}
|
|
1022
|
+
const { resourceMetadataUrl } = extractWWWAuthenticateParams(response);
|
|
1023
|
+
return resourceMetadataUrl;
|
|
1024
|
+
}
|
|
1025
|
+
function defaultStatusWriter(message) {
|
|
1026
|
+
process4.stderr.write(message);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// src/auth/logout.ts
|
|
1030
|
+
import process5 from "process";
|
|
1031
|
+
async function logout(options) {
|
|
1032
|
+
const config = loadConfig({
|
|
1033
|
+
configPath: options.configPath,
|
|
1034
|
+
envFilePath: options.envFilePath
|
|
1035
|
+
});
|
|
1036
|
+
const entry = resolveOAuthCapableEntry2(config, options.mcpName);
|
|
1037
|
+
const writeStatus = options.writeStatus ?? defaultStatusWriter2;
|
|
1038
|
+
const keychain = new KeychainStore(options.mcpName, entry.url);
|
|
1039
|
+
const removed = keychain.delete();
|
|
1040
|
+
if (removed) {
|
|
1041
|
+
writeStatus(`Removed keychain credentials for "${options.mcpName}".
|
|
1042
|
+
`);
|
|
1043
|
+
} else {
|
|
1044
|
+
writeStatus(
|
|
1045
|
+
`No keychain credentials were stored for "${options.mcpName}"; nothing to remove.
|
|
1046
|
+
`
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
function resolveOAuthCapableEntry2(config, mcpName2) {
|
|
1051
|
+
const entry = config.mcp[mcpName2];
|
|
1052
|
+
if (entry === void 0) {
|
|
1053
|
+
const available = Object.keys(config.mcp).sort().join(", ");
|
|
1054
|
+
throw new Error(`Unknown MCP "${mcpName2}". Configured MCPs: ${available || "(none)"}.`);
|
|
1055
|
+
}
|
|
1056
|
+
if (entry.transport !== "streamable-http" && entry.transport !== "sse") {
|
|
1057
|
+
throw new Error(
|
|
1058
|
+
`MCP "${mcpName2}" uses the "${entry.transport}" transport; OAuth is only supported for streamable-http and sse upstreams.`
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
return entry;
|
|
1062
|
+
}
|
|
1063
|
+
function defaultStatusWriter2(message) {
|
|
1064
|
+
process5.stderr.write(message);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/diagnostics/list.ts
|
|
1068
|
+
import process6 from "process";
|
|
1069
|
+
|
|
1070
|
+
// src/diagnostics/format.ts
|
|
1071
|
+
function renderTable(headers, rows) {
|
|
1072
|
+
const allRows = [headers, ...rows];
|
|
1073
|
+
const widths = headers.map(
|
|
1074
|
+
(_, colIdx) => Math.max(...allRows.map((row) => (row[colIdx] ?? "").length))
|
|
1075
|
+
);
|
|
1076
|
+
return allRows.map(
|
|
1077
|
+
(row) => row.map((cell, i) => {
|
|
1078
|
+
if (i === headers.length - 1) return cell;
|
|
1079
|
+
return cell.padEnd(widths[i] ?? 0);
|
|
1080
|
+
}).join(" ").trimEnd()
|
|
1081
|
+
).join("\n");
|
|
1082
|
+
}
|
|
1083
|
+
function truncate(value, max) {
|
|
1084
|
+
if (value.length <= max) return value;
|
|
1085
|
+
if (max <= 3) return value.slice(0, max);
|
|
1086
|
+
return `${value.slice(0, max - 3)}...`;
|
|
1087
|
+
}
|
|
1088
|
+
function humanizeDuration(seconds) {
|
|
1089
|
+
if (seconds < 0) return "expired";
|
|
1090
|
+
if (seconds < 60) return `${Math.floor(seconds)}s`;
|
|
1091
|
+
const totalMinutes = Math.floor(seconds / 60);
|
|
1092
|
+
if (totalMinutes < 60) return `${totalMinutes}m`;
|
|
1093
|
+
const totalHours = Math.floor(totalMinutes / 60);
|
|
1094
|
+
const remainingMinutes = totalMinutes % 60;
|
|
1095
|
+
if (totalHours < 24) {
|
|
1096
|
+
return remainingMinutes > 0 ? `${totalHours}h ${remainingMinutes}m` : `${totalHours}h`;
|
|
1097
|
+
}
|
|
1098
|
+
const totalDays = Math.floor(totalHours / 24);
|
|
1099
|
+
const remainingHours = totalHours % 24;
|
|
1100
|
+
return remainingHours > 0 ? `${totalDays}d ${remainingHours}h` : `${totalDays}d`;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// src/diagnostics/list.ts
|
|
1104
|
+
var ENDPOINT_MAX_WIDTH = 48;
|
|
1105
|
+
async function list(options = {}) {
|
|
1106
|
+
const config = loadConfig({
|
|
1107
|
+
configPath: options.configPath,
|
|
1108
|
+
envFilePath: options.envFilePath
|
|
1109
|
+
});
|
|
1110
|
+
const write = options.write ?? ((chunk) => void process6.stdout.write(chunk));
|
|
1111
|
+
const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
1112
|
+
const entries = buildEntries(config, now);
|
|
1113
|
+
if (options.json === true) {
|
|
1114
|
+
write(`${JSON.stringify(entries, null, 2)}
|
|
1115
|
+
`);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (entries.length === 0) {
|
|
1119
|
+
write("No upstream MCPs configured.\n");
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const headers = ["NAME", "TRANSPORT", "MODE", "ENDPOINT", "AUTH"];
|
|
1123
|
+
const rows = entries.map((entry) => [
|
|
1124
|
+
entry.name,
|
|
1125
|
+
entry.transport,
|
|
1126
|
+
entry.mode,
|
|
1127
|
+
truncate(entry.endpoint, ENDPOINT_MAX_WIDTH),
|
|
1128
|
+
formatAuthStatus(entry.auth)
|
|
1129
|
+
]);
|
|
1130
|
+
write(`${renderTable(headers, rows)}
|
|
1131
|
+
`);
|
|
1132
|
+
}
|
|
1133
|
+
function buildEntries(config, now) {
|
|
1134
|
+
return Object.entries(config.mcp).map(([name, entry]) => {
|
|
1135
|
+
const mode = entry.description !== void 0 ? "lazy" : "eager";
|
|
1136
|
+
if (entry.transport === "stdio") {
|
|
1137
|
+
const command = entry.command;
|
|
1138
|
+
const args = (entry.args ?? []).join(" ");
|
|
1139
|
+
const endpoint = args.length > 0 ? `${command} ${args}` : command;
|
|
1140
|
+
const built2 = {
|
|
1141
|
+
name,
|
|
1142
|
+
transport: "stdio",
|
|
1143
|
+
mode,
|
|
1144
|
+
endpoint,
|
|
1145
|
+
auth: { kind: "n/a" }
|
|
1146
|
+
};
|
|
1147
|
+
if (entry.description !== void 0) built2.description = entry.description;
|
|
1148
|
+
return built2;
|
|
1149
|
+
}
|
|
1150
|
+
const hasAuthHeader = hasBearerAuthHeader(entry.headers);
|
|
1151
|
+
const keychain = new KeychainStore(name, entry.url);
|
|
1152
|
+
const blob = keychain.get();
|
|
1153
|
+
let auth2;
|
|
1154
|
+
if (blob !== void 0) {
|
|
1155
|
+
const expiresInSeconds = blob.expires_at - now();
|
|
1156
|
+
auth2 = {
|
|
1157
|
+
kind: "oauth",
|
|
1158
|
+
status: "logged_in",
|
|
1159
|
+
expiresInSeconds,
|
|
1160
|
+
expiresAt: blob.expires_at
|
|
1161
|
+
};
|
|
1162
|
+
if (hasAuthHeader) auth2.alsoHeader = true;
|
|
1163
|
+
} else if (hasAuthHeader) {
|
|
1164
|
+
auth2 = { kind: "header" };
|
|
1165
|
+
} else {
|
|
1166
|
+
auth2 = { kind: "oauth", status: "not_logged_in" };
|
|
1167
|
+
}
|
|
1168
|
+
const built = {
|
|
1169
|
+
name,
|
|
1170
|
+
transport: entry.transport,
|
|
1171
|
+
mode,
|
|
1172
|
+
endpoint: entry.url,
|
|
1173
|
+
auth: auth2
|
|
1174
|
+
};
|
|
1175
|
+
if (entry.description !== void 0) built.description = entry.description;
|
|
1176
|
+
return built;
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
function hasBearerAuthHeader(headers) {
|
|
1180
|
+
if (headers === void 0) return false;
|
|
1181
|
+
for (const key of Object.keys(headers)) {
|
|
1182
|
+
if (key.toLowerCase() === "authorization") return true;
|
|
1183
|
+
}
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
function formatAuthStatus(auth2) {
|
|
1187
|
+
switch (auth2.kind) {
|
|
1188
|
+
case "n/a":
|
|
1189
|
+
return "n/a";
|
|
1190
|
+
case "header":
|
|
1191
|
+
return "header";
|
|
1192
|
+
case "oauth": {
|
|
1193
|
+
if (auth2.status === "not_logged_in") return "oauth: not logged in";
|
|
1194
|
+
const duration = humanizeDuration(auth2.expiresInSeconds ?? 0);
|
|
1195
|
+
const base = `oauth: logged in (expires in ${duration})`;
|
|
1196
|
+
return auth2.alsoHeader === true ? `${base} (header also set)` : base;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// src/diagnostics/test.ts
|
|
1202
|
+
import process8 from "process";
|
|
1203
|
+
|
|
1204
|
+
// src/proxy/transport-factory.ts
|
|
1205
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
1206
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1207
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
1208
|
+
function createTransport(mcpName2, config) {
|
|
1209
|
+
switch (config.transport) {
|
|
1210
|
+
case "stdio":
|
|
1211
|
+
return new StdioClientTransport({
|
|
1212
|
+
command: config.command,
|
|
1213
|
+
args: config.args,
|
|
1214
|
+
env: config.env
|
|
1215
|
+
});
|
|
1216
|
+
case "streamable-http":
|
|
1217
|
+
return new StreamableHTTPClientTransport(new URL(config.url), {
|
|
1218
|
+
...config.headers !== void 0 ? { requestInit: { headers: config.headers } } : {},
|
|
1219
|
+
authProvider: buildOAuthProvider(mcpName2, config)
|
|
1220
|
+
});
|
|
1221
|
+
case "sse":
|
|
1222
|
+
return new SSEClientTransport(new URL(config.url), {
|
|
1223
|
+
...config.headers !== void 0 ? { requestInit: { headers: config.headers } } : {},
|
|
1224
|
+
authProvider: buildOAuthProvider(mcpName2, config)
|
|
1225
|
+
});
|
|
1226
|
+
default: {
|
|
1227
|
+
const _exhaustive = config;
|
|
1228
|
+
return _exhaustive;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
function buildOAuthProvider(mcpName2, config) {
|
|
1233
|
+
const keychain = new KeychainStore(mcpName2, config.url);
|
|
1234
|
+
return new ProxyOAuthProvider(mcpName2, keychain, config.auth);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// src/proxy/upstream-client.ts
|
|
1238
|
+
import process7 from "process";
|
|
1239
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1240
|
+
import {
|
|
1241
|
+
CreateMessageRequestSchema,
|
|
1242
|
+
ElicitRequestSchema,
|
|
1243
|
+
ListRootsRequestSchema,
|
|
1244
|
+
LoggingMessageNotificationSchema,
|
|
1245
|
+
PromptListChangedNotificationSchema,
|
|
1246
|
+
ResourceListChangedNotificationSchema,
|
|
1247
|
+
ResourceUpdatedNotificationSchema,
|
|
1248
|
+
ToolListChangedNotificationSchema
|
|
1249
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
1250
|
+
var UpstreamClient = class {
|
|
1251
|
+
transport;
|
|
1252
|
+
onTransportError;
|
|
1253
|
+
notificationHandlers;
|
|
1254
|
+
serverRequestHandlers;
|
|
1255
|
+
client = null;
|
|
1256
|
+
constructor({
|
|
1257
|
+
name,
|
|
1258
|
+
transport,
|
|
1259
|
+
onTransportError,
|
|
1260
|
+
notifications,
|
|
1261
|
+
serverRequests
|
|
1262
|
+
}) {
|
|
1263
|
+
this.transport = transport;
|
|
1264
|
+
this.notificationHandlers = notifications ?? {};
|
|
1265
|
+
this.serverRequestHandlers = serverRequests ?? {};
|
|
1266
|
+
this.onTransportError = onTransportError ?? ((error) => {
|
|
1267
|
+
process7.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
|
|
1268
|
+
`);
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
async connect() {
|
|
1272
|
+
this.transport.onerror = this.onTransportError;
|
|
1273
|
+
this.client = new Client(
|
|
1274
|
+
{ name: "dynamic-discovery-mcp", version: "1.0.0" },
|
|
1275
|
+
{
|
|
1276
|
+
capabilities: {
|
|
1277
|
+
// Declare every client-side capability the proxy may relay on behalf of the host.
|
|
1278
|
+
// Actual reachability of each feature depends on what the host supports — if the
|
|
1279
|
+
// host does not support sampling, for instance, the host call returns an error
|
|
1280
|
+
// which we forward back to the upstream verbatim.
|
|
1281
|
+
sampling: {},
|
|
1282
|
+
elicitation: {},
|
|
1283
|
+
roots: { listChanged: true }
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
);
|
|
1287
|
+
this.registerServerRequestHandlers(this.client);
|
|
1288
|
+
if (this.notificationHandlers.onToolsListChanged !== void 0) {
|
|
1289
|
+
this.client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
|
|
1290
|
+
await this.notificationHandlers.onToolsListChanged?.();
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
if (this.notificationHandlers.onResourcesListChanged !== void 0) {
|
|
1294
|
+
this.client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => {
|
|
1295
|
+
await this.notificationHandlers.onResourcesListChanged?.();
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
if (this.notificationHandlers.onResourceUpdated !== void 0) {
|
|
1299
|
+
this.client.setNotificationHandler(ResourceUpdatedNotificationSchema, async (notification) => {
|
|
1300
|
+
await this.notificationHandlers.onResourceUpdated?.({ uri: notification.params.uri });
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
if (this.notificationHandlers.onPromptsListChanged !== void 0) {
|
|
1304
|
+
this.client.setNotificationHandler(PromptListChangedNotificationSchema, async () => {
|
|
1305
|
+
await this.notificationHandlers.onPromptsListChanged?.();
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
if (this.notificationHandlers.onLogMessage !== void 0) {
|
|
1309
|
+
this.client.setNotificationHandler(LoggingMessageNotificationSchema, async (notification) => {
|
|
1310
|
+
await this.notificationHandlers.onLogMessage?.(notification.params);
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
await this.client.connect(this.transport);
|
|
1314
|
+
}
|
|
1315
|
+
async setLoggingLevel(level, options) {
|
|
1316
|
+
const client = this.requireClient();
|
|
1317
|
+
await client.setLoggingLevel(level, options);
|
|
1318
|
+
}
|
|
1319
|
+
async listPrompts(options) {
|
|
1320
|
+
const client = this.requireClient();
|
|
1321
|
+
const result = await client.listPrompts(void 0, options);
|
|
1322
|
+
return result.prompts;
|
|
1323
|
+
}
|
|
1324
|
+
async getPrompt(name, args, options) {
|
|
1325
|
+
const client = this.requireClient();
|
|
1326
|
+
const params = { name };
|
|
1327
|
+
if (args !== void 0) {
|
|
1328
|
+
params.arguments = args;
|
|
1329
|
+
}
|
|
1330
|
+
return client.getPrompt(params, options);
|
|
1331
|
+
}
|
|
1332
|
+
async complete(params, options) {
|
|
1333
|
+
const client = this.requireClient();
|
|
1334
|
+
return client.complete(params, options);
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Returns the capabilities advertised by the upstream server during initialize.
|
|
1338
|
+
* Returns `undefined` if the client is not connected, or if the SDK has not yet
|
|
1339
|
+
* recorded the server's capabilities (e.g. during a partially-completed handshake).
|
|
1340
|
+
*/
|
|
1341
|
+
getCapabilities() {
|
|
1342
|
+
return this.client?.getServerCapabilities();
|
|
1343
|
+
}
|
|
1344
|
+
async listTools(options) {
|
|
1345
|
+
const client = this.requireClient();
|
|
1346
|
+
const result = await client.listTools(void 0, options);
|
|
1347
|
+
return result.tools.map((tool) => {
|
|
1348
|
+
const upstreamTool = {
|
|
1349
|
+
name: tool.name,
|
|
1350
|
+
description: tool.description ?? "",
|
|
1351
|
+
inputSchema: tool.inputSchema
|
|
1352
|
+
};
|
|
1353
|
+
if (tool.outputSchema !== void 0) {
|
|
1354
|
+
upstreamTool.outputSchema = tool.outputSchema;
|
|
1355
|
+
}
|
|
1356
|
+
if (tool.annotations !== void 0) {
|
|
1357
|
+
upstreamTool.annotations = {
|
|
1358
|
+
title: tool.annotations.title,
|
|
1359
|
+
readOnlyHint: tool.annotations.readOnlyHint,
|
|
1360
|
+
destructiveHint: tool.annotations.destructiveHint,
|
|
1361
|
+
idempotentHint: tool.annotations.idempotentHint,
|
|
1362
|
+
openWorldHint: tool.annotations.openWorldHint
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
return upstreamTool;
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
async callTool(name, input, options) {
|
|
1369
|
+
const client = this.requireClient();
|
|
1370
|
+
const result = await client.callTool({ name, arguments: input }, void 0, options);
|
|
1371
|
+
return result;
|
|
1372
|
+
}
|
|
1373
|
+
async listResources(options) {
|
|
1374
|
+
const client = this.requireClient();
|
|
1375
|
+
const result = await client.listResources(void 0, options);
|
|
1376
|
+
return result.resources;
|
|
1377
|
+
}
|
|
1378
|
+
async listResourceTemplates(options) {
|
|
1379
|
+
const client = this.requireClient();
|
|
1380
|
+
const result = await client.listResourceTemplates(void 0, options);
|
|
1381
|
+
return result.resourceTemplates;
|
|
1382
|
+
}
|
|
1383
|
+
async readResource(uri, options) {
|
|
1384
|
+
const client = this.requireClient();
|
|
1385
|
+
return client.readResource({ uri }, options);
|
|
1386
|
+
}
|
|
1387
|
+
async subscribeResource(uri, options) {
|
|
1388
|
+
const client = this.requireClient();
|
|
1389
|
+
await client.subscribeResource({ uri }, options);
|
|
1390
|
+
}
|
|
1391
|
+
async unsubscribeResource(uri, options) {
|
|
1392
|
+
const client = this.requireClient();
|
|
1393
|
+
await client.unsubscribeResource({ uri }, options);
|
|
1394
|
+
}
|
|
1395
|
+
async disconnect() {
|
|
1396
|
+
if (this.client === null) {
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
await this.client.close();
|
|
1400
|
+
this.client = null;
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Sends `notifications/roots/list_changed` to the upstream, letting it know that
|
|
1404
|
+
* the host's set of filesystem roots has changed.
|
|
1405
|
+
*/
|
|
1406
|
+
async sendRootsListChanged() {
|
|
1407
|
+
const client = this.requireClient();
|
|
1408
|
+
await client.sendRootsListChanged();
|
|
1409
|
+
}
|
|
1410
|
+
registerServerRequestHandlers(client) {
|
|
1411
|
+
if (this.serverRequestHandlers.onCreateMessage !== void 0) {
|
|
1412
|
+
client.setRequestHandler(
|
|
1413
|
+
CreateMessageRequestSchema,
|
|
1414
|
+
async (request, extra) => {
|
|
1415
|
+
return this.serverRequestHandlers.onCreateMessage(request.params, {
|
|
1416
|
+
signal: extra.signal
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
if (this.serverRequestHandlers.onElicitInput !== void 0) {
|
|
1422
|
+
client.setRequestHandler(
|
|
1423
|
+
ElicitRequestSchema,
|
|
1424
|
+
async (request, extra) => {
|
|
1425
|
+
return this.serverRequestHandlers.onElicitInput(request.params, {
|
|
1426
|
+
signal: extra.signal
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
if (this.serverRequestHandlers.onListRoots !== void 0) {
|
|
1432
|
+
client.setRequestHandler(
|
|
1433
|
+
ListRootsRequestSchema,
|
|
1434
|
+
async (request, extra) => {
|
|
1435
|
+
return this.serverRequestHandlers.onListRoots(request.params, {
|
|
1436
|
+
signal: extra.signal
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
requireClient() {
|
|
1443
|
+
if (this.client === null) {
|
|
1444
|
+
throw new Error("Client is not connected. Call connect() first.");
|
|
1445
|
+
}
|
|
1446
|
+
return this.client;
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
// src/diagnostics/test.ts
|
|
1451
|
+
var DESCRIPTION_MAX_LENGTH = 100;
|
|
1452
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
1453
|
+
async function test(options = {}) {
|
|
1454
|
+
const config = loadConfig({
|
|
1455
|
+
configPath: options.configPath,
|
|
1456
|
+
envFilePath: options.envFilePath
|
|
1457
|
+
});
|
|
1458
|
+
const write = options.write ?? ((chunk) => void process8.stdout.write(chunk));
|
|
1459
|
+
const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
1460
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1461
|
+
if (options.mcpName !== void 0) {
|
|
1462
|
+
return runSingle(config, options.mcpName, {
|
|
1463
|
+
write,
|
|
1464
|
+
now,
|
|
1465
|
+
timeoutMs,
|
|
1466
|
+
json: options.json === true
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
return runAll(config, { write, now, timeoutMs, json: options.json === true });
|
|
1470
|
+
}
|
|
1471
|
+
async function runSingle(config, mcpName2, options) {
|
|
1472
|
+
const entry = config.mcp[mcpName2];
|
|
1473
|
+
if (entry === void 0) {
|
|
1474
|
+
const available = Object.keys(config.mcp).sort().join(", ");
|
|
1475
|
+
throw new Error(`Unknown MCP "${mcpName2}". Configured MCPs: ${available || "(none)"}.`);
|
|
1476
|
+
}
|
|
1477
|
+
if (!options.json) {
|
|
1478
|
+
options.write(`Testing "${mcpName2}" (${entry.transport}, ${endpointForEntry(entry)})
|
|
1479
|
+
`);
|
|
1480
|
+
}
|
|
1481
|
+
const result = await probeOne(mcpName2, entry, options.timeoutMs, options.now);
|
|
1482
|
+
if (options.json) {
|
|
1483
|
+
options.write(`${JSON.stringify(result, null, 2)}
|
|
1484
|
+
`);
|
|
1485
|
+
} else {
|
|
1486
|
+
for (const step of result.steps) {
|
|
1487
|
+
options.write(` [${step.status}] ${step.label}
|
|
1488
|
+
`);
|
|
1489
|
+
if (step.error !== void 0) {
|
|
1490
|
+
options.write(` -> ${step.error}
|
|
1491
|
+
`);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
renderDiscoveredSurface(options.write, result);
|
|
1495
|
+
options.write(`Result: ${result.result}
|
|
1496
|
+
`);
|
|
1497
|
+
}
|
|
1498
|
+
return result.result === "PASS" ? 0 : 1;
|
|
1499
|
+
}
|
|
1500
|
+
async function runAll(config, options) {
|
|
1501
|
+
const names = Object.keys(config.mcp);
|
|
1502
|
+
const total = names.length;
|
|
1503
|
+
if (!options.json) {
|
|
1504
|
+
options.write(`Testing all configured upstreams (${total})...
|
|
1505
|
+
|
|
1506
|
+
`);
|
|
1507
|
+
}
|
|
1508
|
+
const results = [];
|
|
1509
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
1510
|
+
const name = names[index];
|
|
1511
|
+
const entry = config.mcp[name];
|
|
1512
|
+
if (!options.json) {
|
|
1513
|
+
options.write(`[${index + 1}/${total}] ${name} (${entry.transport}) ... `);
|
|
1514
|
+
}
|
|
1515
|
+
const result = await probeOne(name, entry, options.timeoutMs, options.now);
|
|
1516
|
+
results.push(result);
|
|
1517
|
+
if (!options.json) {
|
|
1518
|
+
if (result.result === "PASS") {
|
|
1519
|
+
const counts = `${result.tools?.length ?? 0} tools, ${(result.resources?.length ?? 0) + (result.resource_templates?.length ?? 0)} resources, ${result.prompts?.length ?? 0} prompts`;
|
|
1520
|
+
options.write(`PASS (${counts})
|
|
1521
|
+
`);
|
|
1522
|
+
} else {
|
|
1523
|
+
options.write(`FAIL (${result.fail_reason ?? "unknown"})
|
|
1524
|
+
`);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
const passed = results.filter((r) => r.result === "PASS").length;
|
|
1529
|
+
const failed = results.length - passed;
|
|
1530
|
+
if (options.json) {
|
|
1531
|
+
const payload = { summary: { passed, failed }, results };
|
|
1532
|
+
options.write(`${JSON.stringify(payload, null, 2)}
|
|
1533
|
+
`);
|
|
1534
|
+
} else {
|
|
1535
|
+
options.write(`
|
|
1536
|
+
Summary: ${passed} passed, ${failed} failed
|
|
1537
|
+
`);
|
|
1538
|
+
}
|
|
1539
|
+
return failed === 0 ? 0 : 1;
|
|
1540
|
+
}
|
|
1541
|
+
async function probeOne(mcpName2, entry, timeoutMs, now) {
|
|
1542
|
+
const result = {
|
|
1543
|
+
name: mcpName2,
|
|
1544
|
+
result: "PASS",
|
|
1545
|
+
transport: entry.transport,
|
|
1546
|
+
endpoint: endpointForEntry(entry),
|
|
1547
|
+
auth: deriveAuthSummary(mcpName2, entry, now),
|
|
1548
|
+
steps: []
|
|
1549
|
+
};
|
|
1550
|
+
if (entry.transport !== "stdio") {
|
|
1551
|
+
result.steps.push({ label: authStepLabel(result.auth), status: "ok" });
|
|
1552
|
+
}
|
|
1553
|
+
const clientHolder = { value: null };
|
|
1554
|
+
let timeoutHandle;
|
|
1555
|
+
const run = (async () => {
|
|
1556
|
+
const transport = createTransport(mcpName2, entry);
|
|
1557
|
+
const client = new UpstreamClient({
|
|
1558
|
+
name: mcpName2,
|
|
1559
|
+
transport,
|
|
1560
|
+
onTransportError: () => {
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
clientHolder.value = client;
|
|
1564
|
+
await client.connect();
|
|
1565
|
+
result.steps.push({ label: "Connected and initialized", status: "ok" });
|
|
1566
|
+
const caps = client.getCapabilities();
|
|
1567
|
+
result.capabilities = caps;
|
|
1568
|
+
result.steps.push({
|
|
1569
|
+
label: `Capabilities: ${describeCapabilities(caps)}`,
|
|
1570
|
+
status: "ok"
|
|
1571
|
+
});
|
|
1572
|
+
const tools = await client.listTools();
|
|
1573
|
+
result.tools = tools.map((tool) => ({
|
|
1574
|
+
name: tool.name,
|
|
1575
|
+
description: tool.description
|
|
1576
|
+
}));
|
|
1577
|
+
result.steps.push({
|
|
1578
|
+
label: `tools/list returned ${tools.length} tool${tools.length === 1 ? "" : "s"}`,
|
|
1579
|
+
status: "ok"
|
|
1580
|
+
});
|
|
1581
|
+
let resources = [];
|
|
1582
|
+
let templates = [];
|
|
1583
|
+
if (caps?.resources !== void 0) {
|
|
1584
|
+
try {
|
|
1585
|
+
resources = await client.listResources();
|
|
1586
|
+
templates = await client.listResourceTemplates();
|
|
1587
|
+
result.resources = resources.map((r) => {
|
|
1588
|
+
const out = {
|
|
1589
|
+
uri: r.uri,
|
|
1590
|
+
name: r.name
|
|
1591
|
+
};
|
|
1592
|
+
if (r.description !== void 0) out.description = r.description;
|
|
1593
|
+
return out;
|
|
1594
|
+
});
|
|
1595
|
+
result.resource_templates = templates.map((t) => {
|
|
1596
|
+
const out = {
|
|
1597
|
+
uriTemplate: t.uriTemplate,
|
|
1598
|
+
name: t.name
|
|
1599
|
+
};
|
|
1600
|
+
if (t.description !== void 0) out.description = t.description;
|
|
1601
|
+
return out;
|
|
1602
|
+
});
|
|
1603
|
+
result.steps.push({
|
|
1604
|
+
label: `resources/list returned ${resources.length} resource${resources.length === 1 ? "" : "s"}, ${templates.length} template${templates.length === 1 ? "" : "s"}`,
|
|
1605
|
+
status: "ok"
|
|
1606
|
+
});
|
|
1607
|
+
} catch (error) {
|
|
1608
|
+
result.steps.push({
|
|
1609
|
+
label: "resources/list",
|
|
1610
|
+
status: "fail",
|
|
1611
|
+
error: errorMessage(error)
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
let prompts = [];
|
|
1616
|
+
if (caps?.prompts !== void 0) {
|
|
1617
|
+
try {
|
|
1618
|
+
prompts = await client.listPrompts();
|
|
1619
|
+
result.prompts = prompts.map((p) => {
|
|
1620
|
+
const out = { name: p.name };
|
|
1621
|
+
if (p.description !== void 0) out.description = p.description;
|
|
1622
|
+
return out;
|
|
1623
|
+
});
|
|
1624
|
+
result.steps.push({
|
|
1625
|
+
label: `prompts/list returned ${prompts.length} prompt${prompts.length === 1 ? "" : "s"}`,
|
|
1626
|
+
status: "ok"
|
|
1627
|
+
});
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
result.steps.push({
|
|
1630
|
+
label: "prompts/list",
|
|
1631
|
+
status: "fail",
|
|
1632
|
+
error: errorMessage(error)
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
})();
|
|
1637
|
+
const timeout = new Promise((_, reject) => {
|
|
1638
|
+
timeoutHandle = setTimeout(() => {
|
|
1639
|
+
reject(new TestTimeoutError(timeoutMs));
|
|
1640
|
+
}, timeoutMs);
|
|
1641
|
+
});
|
|
1642
|
+
try {
|
|
1643
|
+
await Promise.race([run, timeout]);
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
result.result = "FAIL";
|
|
1646
|
+
result.fail_reason = failReason(error, mcpName2);
|
|
1647
|
+
result.steps.push({
|
|
1648
|
+
label: `aborted: ${result.fail_reason}`,
|
|
1649
|
+
status: "fail",
|
|
1650
|
+
error: errorMessage(error)
|
|
1651
|
+
});
|
|
1652
|
+
} finally {
|
|
1653
|
+
if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
|
|
1654
|
+
const connected = clientHolder.value;
|
|
1655
|
+
if (connected !== null) {
|
|
1656
|
+
try {
|
|
1657
|
+
await connected.disconnect();
|
|
1658
|
+
} catch {
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
if (result.result === "PASS" && result.steps.some((s) => s.status === "fail")) {
|
|
1663
|
+
result.result = "FAIL";
|
|
1664
|
+
const failed = result.steps.find((s) => s.status === "fail");
|
|
1665
|
+
result.fail_reason = failed?.error ?? failed?.label ?? "unknown";
|
|
1666
|
+
}
|
|
1667
|
+
return result;
|
|
1668
|
+
}
|
|
1669
|
+
var TestTimeoutError = class extends Error {
|
|
1670
|
+
constructor(timeoutMs) {
|
|
1671
|
+
super(`Test timed out after ${timeoutMs}ms`);
|
|
1672
|
+
this.name = "TestTimeoutError";
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
function endpointForEntry(entry) {
|
|
1676
|
+
if (entry.transport === "stdio") {
|
|
1677
|
+
const args = (entry.args ?? []).join(" ");
|
|
1678
|
+
return args.length > 0 ? `${entry.command} ${args}` : entry.command;
|
|
1679
|
+
}
|
|
1680
|
+
return entry.url;
|
|
1681
|
+
}
|
|
1682
|
+
function deriveAuthSummary(mcpName2, entry, now) {
|
|
1683
|
+
if (entry.transport === "stdio") return { kind: "n/a" };
|
|
1684
|
+
const keychain = new KeychainStore(mcpName2, entry.url);
|
|
1685
|
+
const blob = keychain.get();
|
|
1686
|
+
if (blob !== void 0) {
|
|
1687
|
+
return {
|
|
1688
|
+
kind: "oauth",
|
|
1689
|
+
status: "valid",
|
|
1690
|
+
expiresInSeconds: blob.expires_at - now()
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
const hasHeader = entry.headers !== void 0 && Object.keys(entry.headers).some((k) => k.toLowerCase() === "authorization");
|
|
1694
|
+
if (hasHeader) return { kind: "header" };
|
|
1695
|
+
return { kind: "oauth", status: "missing" };
|
|
1696
|
+
}
|
|
1697
|
+
function authStepLabel(auth2) {
|
|
1698
|
+
switch (auth2.kind) {
|
|
1699
|
+
case "n/a":
|
|
1700
|
+
return "(no auth applicable)";
|
|
1701
|
+
case "header":
|
|
1702
|
+
return "Static Authorization header present";
|
|
1703
|
+
case "oauth":
|
|
1704
|
+
if (auth2.status === "missing") return "No cached OAuth token";
|
|
1705
|
+
return `OAuth token present (expires in ${humanizeDuration(auth2.expiresInSeconds ?? 0)})`;
|
|
352
1706
|
}
|
|
353
|
-
return result.data;
|
|
354
1707
|
}
|
|
355
|
-
function
|
|
356
|
-
if (
|
|
357
|
-
|
|
1708
|
+
function describeCapabilities(caps) {
|
|
1709
|
+
if (caps === void 0) return "(none advertised)";
|
|
1710
|
+
const parts = [];
|
|
1711
|
+
for (const [name, value] of Object.entries(caps)) {
|
|
1712
|
+
if (value === void 0 || value === null) continue;
|
|
1713
|
+
if (typeof value === "object" && Object.keys(value).length > 0) {
|
|
1714
|
+
const flags = Object.entries(value).filter(([, v]) => v === true).map(([k]) => k).join(",");
|
|
1715
|
+
parts.push(flags.length > 0 ? `${name}(${flags})` : name);
|
|
1716
|
+
} else {
|
|
1717
|
+
parts.push(name);
|
|
1718
|
+
}
|
|
358
1719
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
1720
|
+
return parts.length > 0 ? parts.join(", ") : "(none advertised)";
|
|
1721
|
+
}
|
|
1722
|
+
function failReason(error, mcpName2) {
|
|
1723
|
+
if (isAuthRequiredError(error)) {
|
|
1724
|
+
return `auth required: run \`dynmcp login ${mcpName2}\``;
|
|
363
1725
|
}
|
|
364
|
-
return
|
|
1726
|
+
if (error instanceof TestTimeoutError) return error.message;
|
|
1727
|
+
if (error instanceof Error) return error.message.split("\n")[0] ?? "unknown error";
|
|
1728
|
+
return String(error);
|
|
365
1729
|
}
|
|
366
|
-
function
|
|
367
|
-
|
|
1730
|
+
function errorMessage(error) {
|
|
1731
|
+
if (error instanceof Error) return error.message;
|
|
1732
|
+
return String(error);
|
|
1733
|
+
}
|
|
1734
|
+
function renderDiscoveredSurface(write, result) {
|
|
1735
|
+
if (result.result !== "PASS") return;
|
|
1736
|
+
const sections = [
|
|
1737
|
+
[
|
|
1738
|
+
`Tools (${result.tools?.length ?? 0})`,
|
|
1739
|
+
result.tools && result.tools.length > 0 ? () => {
|
|
1740
|
+
const sorted = [...result.tools ?? []].sort((a, b) => a.name.localeCompare(b.name));
|
|
1741
|
+
for (const tool of sorted) {
|
|
1742
|
+
write(` - ${tool.name}: ${truncate(tool.description, DESCRIPTION_MAX_LENGTH)}
|
|
1743
|
+
`);
|
|
1744
|
+
}
|
|
1745
|
+
} : void 0
|
|
1746
|
+
],
|
|
1747
|
+
[
|
|
1748
|
+
`Resources (${result.resources?.length ?? 0})`,
|
|
1749
|
+
result.resources && result.resources.length > 0 ? () => {
|
|
1750
|
+
const sorted = [...result.resources ?? []].sort((a, b) => a.uri.localeCompare(b.uri));
|
|
1751
|
+
for (const r of sorted) {
|
|
1752
|
+
const tail = r.description ?? r.name;
|
|
1753
|
+
write(` - ${r.uri}: ${truncate(tail, DESCRIPTION_MAX_LENGTH)}
|
|
1754
|
+
`);
|
|
1755
|
+
}
|
|
1756
|
+
} : void 0
|
|
1757
|
+
],
|
|
1758
|
+
[
|
|
1759
|
+
`Resource templates (${result.resource_templates?.length ?? 0})`,
|
|
1760
|
+
result.resource_templates && result.resource_templates.length > 0 ? () => {
|
|
1761
|
+
const sorted = [...result.resource_templates ?? []].sort(
|
|
1762
|
+
(a, b) => a.uriTemplate.localeCompare(b.uriTemplate)
|
|
1763
|
+
);
|
|
1764
|
+
for (const t of sorted) {
|
|
1765
|
+
const tail = t.description ?? t.name;
|
|
1766
|
+
write(` - ${t.uriTemplate}: ${truncate(tail, DESCRIPTION_MAX_LENGTH)}
|
|
1767
|
+
`);
|
|
1768
|
+
}
|
|
1769
|
+
} : void 0
|
|
1770
|
+
],
|
|
1771
|
+
[
|
|
1772
|
+
`Prompts (${result.prompts?.length ?? 0})`,
|
|
1773
|
+
result.prompts && result.prompts.length > 0 ? () => {
|
|
1774
|
+
const sorted = [...result.prompts ?? []].sort((a, b) => a.name.localeCompare(b.name));
|
|
1775
|
+
for (const p of sorted) {
|
|
1776
|
+
const tail = p.description ?? "";
|
|
1777
|
+
write(
|
|
1778
|
+
` - ${p.name}${tail.length > 0 ? `: ${truncate(tail, DESCRIPTION_MAX_LENGTH)}` : ""}
|
|
1779
|
+
`
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
} : void 0
|
|
1783
|
+
]
|
|
1784
|
+
];
|
|
1785
|
+
for (const [header, render] of sections) {
|
|
1786
|
+
if (render === void 0) continue;
|
|
1787
|
+
write(`
|
|
1788
|
+
${header}:
|
|
1789
|
+
`);
|
|
1790
|
+
render();
|
|
1791
|
+
}
|
|
368
1792
|
}
|
|
369
1793
|
|
|
370
|
-
// src/
|
|
371
|
-
import
|
|
1794
|
+
// src/proxy/index.ts
|
|
1795
|
+
import process11 from "process";
|
|
1796
|
+
import { StdioClientTransport as StdioClientTransport2 } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
372
1797
|
|
|
373
1798
|
// src/proxy/orchestrator.ts
|
|
374
|
-
import
|
|
1799
|
+
import process9 from "process";
|
|
375
1800
|
|
|
376
1801
|
// src/proxy/capability-aggregator.ts
|
|
377
1802
|
function aggregateCapabilities(upstreams) {
|
|
@@ -749,340 +2174,127 @@ var ToolCatalog = class _ToolCatalog {
|
|
|
749
2174
|
this.tools = tools;
|
|
750
2175
|
this.discoverToolDescription = description2;
|
|
751
2176
|
}
|
|
752
|
-
static fromFlat(upstreamTools) {
|
|
753
|
-
const toolMap = /* @__PURE__ */ new Map();
|
|
754
|
-
for (const tool of upstreamTools) {
|
|
755
|
-
toolMap.set(tool.name, tool);
|
|
756
|
-
}
|
|
757
|
-
const description2 = buildFlatDescription(upstreamTools);
|
|
758
|
-
return new _ToolCatalog(toolMap, description2);
|
|
759
|
-
}
|
|
760
|
-
static fromGrouped(groups) {
|
|
761
|
-
return _ToolCatalog.fromGroupedWithLazy(groups, /* @__PURE__ */ new Map());
|
|
762
|
-
}
|
|
763
|
-
/**
|
|
764
|
-
* Same as {@link fromGrouped} but additionally accepts a map of lazy upstream MCPs
|
|
765
|
-
* (those declared with a `description` field but not yet loaded). When the map is
|
|
766
|
-
* non-empty, the rendered `discover_tool` description includes a `<mcp_servers>`
|
|
767
|
-
* block listing them with their descriptions and an explanatory paragraph telling
|
|
768
|
-
* the agent how to call `load_mcp`. When `groups` is empty, the `<tools>` block is
|
|
769
|
-
* omitted in favor of a trailing sentence directing the agent to `load_mcp`.
|
|
770
|
-
*/
|
|
771
|
-
static fromGroupedWithLazy(groups, lazyDescriptions) {
|
|
772
|
-
const toolMap = /* @__PURE__ */ new Map();
|
|
773
|
-
for (const [mcpName2, tools] of groups) {
|
|
774
|
-
for (const tool of tools) {
|
|
775
|
-
toolMap.set(`${mcpName2}/${tool.name}`, tool);
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
const description2 = buildGroupedDescription(groups, lazyDescriptions);
|
|
779
|
-
return new _ToolCatalog(toolMap, description2);
|
|
780
|
-
}
|
|
781
|
-
getToolDetails(toolName) {
|
|
782
|
-
const tool = this.tools.get(toolName);
|
|
783
|
-
if (tool === void 0) {
|
|
784
|
-
const sortedNames = [...this.tools.keys()].sort().join(", ");
|
|
785
|
-
return `Unknown tool: "${toolName}". Available tools: ${sortedNames}`;
|
|
786
|
-
}
|
|
787
|
-
return buildToolDetailsString(toolName, tool);
|
|
788
|
-
}
|
|
789
|
-
};
|
|
790
|
-
function buildFlatDescription(tools) {
|
|
791
|
-
const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
792
|
-
const toolLines = sortedTools.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
|
|
793
|
-
return `${DISCOVER_TOOL_PREAMBLE}
|
|
794
|
-
|
|
795
|
-
<tools>
|
|
796
|
-
${toolLines}
|
|
797
|
-
</tools>`;
|
|
798
|
-
}
|
|
799
|
-
function buildGroupedDescription(groups, lazyDescriptions) {
|
|
800
|
-
const parts = [DISCOVER_TOOL_PREAMBLE];
|
|
801
|
-
if (lazyDescriptions.size > 0) {
|
|
802
|
-
parts.push(DYNAMIC_DISCOVERY_PREAMBLE);
|
|
803
|
-
parts.push(buildMcpServersBlock(lazyDescriptions));
|
|
804
|
-
}
|
|
805
|
-
if (groups.size > 0) {
|
|
806
|
-
parts.push(buildToolsBlock(groups));
|
|
807
|
-
} else if (lazyDescriptions.size > 0) {
|
|
808
|
-
parts.push(NO_TOOLS_LOADED_FOOTER);
|
|
809
|
-
} else {
|
|
810
|
-
parts.push("<tools>\n</tools>");
|
|
811
|
-
}
|
|
812
|
-
return parts.join("\n\n");
|
|
813
|
-
}
|
|
814
|
-
function buildToolsBlock(groups) {
|
|
815
|
-
const sortedMcpNames = [...groups.keys()].sort();
|
|
816
|
-
const sections = sortedMcpNames.map((mcpName2) => {
|
|
817
|
-
const tools = groups.get(mcpName2);
|
|
818
|
-
const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
819
|
-
const toolLines = sortedTools.map((tool) => `- ${mcpName2}/${tool.name}: ${tool.description}`).join("\n");
|
|
820
|
-
return `${mcpName2}:
|
|
821
|
-
${toolLines}`;
|
|
822
|
-
});
|
|
823
|
-
return `<tools>
|
|
824
|
-
${sections.join("\n\n")}
|
|
825
|
-
</tools>`;
|
|
826
|
-
}
|
|
827
|
-
function buildMcpServersBlock(lazyDescriptions) {
|
|
828
|
-
const lines = [...lazyDescriptions].map(([name, desc]) => `- ${name}: ${desc}`).join("\n");
|
|
829
|
-
return `<mcp_servers>
|
|
830
|
-
${lines}
|
|
831
|
-
</mcp_servers>`;
|
|
832
|
-
}
|
|
833
|
-
function buildToolDetailsString(displayName, tool) {
|
|
834
|
-
const lines = [
|
|
835
|
-
`Tool: ${displayName}`,
|
|
836
|
-
`Description: ${tool.description}`,
|
|
837
|
-
"",
|
|
838
|
-
"Input Schema:",
|
|
839
|
-
JSON.stringify(tool.inputSchema, null, 2)
|
|
840
|
-
];
|
|
841
|
-
if (tool.outputSchema !== void 0) {
|
|
842
|
-
lines.push("", "Output Schema:", JSON.stringify(tool.outputSchema, null, 2));
|
|
843
|
-
}
|
|
844
|
-
const annotationLines = buildAnnotationLines(tool);
|
|
845
|
-
if (annotationLines.length > 0) {
|
|
846
|
-
lines.push("", "Annotations:", ...annotationLines);
|
|
847
|
-
}
|
|
848
|
-
return lines.join("\n");
|
|
849
|
-
}
|
|
850
|
-
function buildAnnotationLines(tool) {
|
|
851
|
-
if (tool.annotations === void 0) {
|
|
852
|
-
return [];
|
|
853
|
-
}
|
|
854
|
-
const { annotations } = tool;
|
|
855
|
-
const lines = [];
|
|
856
|
-
if (annotations.title !== void 0) {
|
|
857
|
-
lines.push(`- title: ${annotations.title}`);
|
|
858
|
-
}
|
|
859
|
-
if (annotations.readOnlyHint !== void 0) {
|
|
860
|
-
lines.push(`- readOnlyHint: ${annotations.readOnlyHint}`);
|
|
861
|
-
}
|
|
862
|
-
if (annotations.destructiveHint !== void 0) {
|
|
863
|
-
lines.push(`- destructiveHint: ${annotations.destructiveHint}`);
|
|
864
|
-
}
|
|
865
|
-
if (annotations.idempotentHint !== void 0) {
|
|
866
|
-
lines.push(`- idempotentHint: ${annotations.idempotentHint}`);
|
|
867
|
-
}
|
|
868
|
-
if (annotations.openWorldHint !== void 0) {
|
|
869
|
-
lines.push(`- openWorldHint: ${annotations.openWorldHint}`);
|
|
870
|
-
}
|
|
871
|
-
return lines;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// src/proxy/upstream-client.ts
|
|
875
|
-
import process3 from "process";
|
|
876
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
877
|
-
import {
|
|
878
|
-
CreateMessageRequestSchema,
|
|
879
|
-
ElicitRequestSchema,
|
|
880
|
-
ListRootsRequestSchema,
|
|
881
|
-
LoggingMessageNotificationSchema,
|
|
882
|
-
PromptListChangedNotificationSchema,
|
|
883
|
-
ResourceListChangedNotificationSchema,
|
|
884
|
-
ResourceUpdatedNotificationSchema,
|
|
885
|
-
ToolListChangedNotificationSchema
|
|
886
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
887
|
-
var UpstreamClient = class {
|
|
888
|
-
transport;
|
|
889
|
-
onTransportError;
|
|
890
|
-
notificationHandlers;
|
|
891
|
-
serverRequestHandlers;
|
|
892
|
-
client = null;
|
|
893
|
-
constructor({
|
|
894
|
-
name,
|
|
895
|
-
transport,
|
|
896
|
-
onTransportError,
|
|
897
|
-
notifications,
|
|
898
|
-
serverRequests
|
|
899
|
-
}) {
|
|
900
|
-
this.transport = transport;
|
|
901
|
-
this.notificationHandlers = notifications ?? {};
|
|
902
|
-
this.serverRequestHandlers = serverRequests ?? {};
|
|
903
|
-
this.onTransportError = onTransportError ?? ((error) => {
|
|
904
|
-
process3.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
|
|
905
|
-
`);
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
async connect() {
|
|
909
|
-
this.transport.onerror = this.onTransportError;
|
|
910
|
-
this.client = new Client(
|
|
911
|
-
{ name: "dynamic-discovery-mcp", version: "1.0.0" },
|
|
912
|
-
{
|
|
913
|
-
capabilities: {
|
|
914
|
-
// Declare every client-side capability the proxy may relay on behalf of the host.
|
|
915
|
-
// Actual reachability of each feature depends on what the host supports — if the
|
|
916
|
-
// host does not support sampling, for instance, the host call returns an error
|
|
917
|
-
// which we forward back to the upstream verbatim.
|
|
918
|
-
sampling: {},
|
|
919
|
-
elicitation: {},
|
|
920
|
-
roots: { listChanged: true }
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
);
|
|
924
|
-
this.registerServerRequestHandlers(this.client);
|
|
925
|
-
if (this.notificationHandlers.onToolsListChanged !== void 0) {
|
|
926
|
-
this.client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
|
|
927
|
-
await this.notificationHandlers.onToolsListChanged?.();
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
if (this.notificationHandlers.onResourcesListChanged !== void 0) {
|
|
931
|
-
this.client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => {
|
|
932
|
-
await this.notificationHandlers.onResourcesListChanged?.();
|
|
933
|
-
});
|
|
934
|
-
}
|
|
935
|
-
if (this.notificationHandlers.onResourceUpdated !== void 0) {
|
|
936
|
-
this.client.setNotificationHandler(ResourceUpdatedNotificationSchema, async (notification) => {
|
|
937
|
-
await this.notificationHandlers.onResourceUpdated?.({ uri: notification.params.uri });
|
|
938
|
-
});
|
|
939
|
-
}
|
|
940
|
-
if (this.notificationHandlers.onPromptsListChanged !== void 0) {
|
|
941
|
-
this.client.setNotificationHandler(PromptListChangedNotificationSchema, async () => {
|
|
942
|
-
await this.notificationHandlers.onPromptsListChanged?.();
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
if (this.notificationHandlers.onLogMessage !== void 0) {
|
|
946
|
-
this.client.setNotificationHandler(LoggingMessageNotificationSchema, async (notification) => {
|
|
947
|
-
await this.notificationHandlers.onLogMessage?.(notification.params);
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
await this.client.connect(this.transport);
|
|
951
|
-
}
|
|
952
|
-
async setLoggingLevel(level, options) {
|
|
953
|
-
const client = this.requireClient();
|
|
954
|
-
await client.setLoggingLevel(level, options);
|
|
955
|
-
}
|
|
956
|
-
async listPrompts(options) {
|
|
957
|
-
const client = this.requireClient();
|
|
958
|
-
const result = await client.listPrompts(void 0, options);
|
|
959
|
-
return result.prompts;
|
|
960
|
-
}
|
|
961
|
-
async getPrompt(name, args, options) {
|
|
962
|
-
const client = this.requireClient();
|
|
963
|
-
const params = { name };
|
|
964
|
-
if (args !== void 0) {
|
|
965
|
-
params.arguments = args;
|
|
2177
|
+
static fromFlat(upstreamTools) {
|
|
2178
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
2179
|
+
for (const tool of upstreamTools) {
|
|
2180
|
+
toolMap.set(tool.name, tool);
|
|
966
2181
|
}
|
|
967
|
-
|
|
2182
|
+
const description2 = buildFlatDescription(upstreamTools);
|
|
2183
|
+
return new _ToolCatalog(toolMap, description2);
|
|
968
2184
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
return client.complete(params, options);
|
|
2185
|
+
static fromGrouped(groups) {
|
|
2186
|
+
return _ToolCatalog.fromGroupedWithLazy(groups, /* @__PURE__ */ new Map());
|
|
972
2187
|
}
|
|
973
2188
|
/**
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
2189
|
+
* Same as {@link fromGrouped} but additionally accepts a map of lazy upstream MCPs
|
|
2190
|
+
* (those declared with a `description` field but not yet loaded). When the map is
|
|
2191
|
+
* non-empty, the rendered `discover_tool` description includes a `<mcp_servers>`
|
|
2192
|
+
* block listing them with their descriptions and an explanatory paragraph telling
|
|
2193
|
+
* the agent how to call `load_mcp`. When `groups` is empty, the `<tools>` block is
|
|
2194
|
+
* omitted in favor of a trailing sentence directing the agent to `load_mcp`.
|
|
977
2195
|
*/
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
const result = await client.listTools(void 0, options);
|
|
984
|
-
return result.tools.map((tool) => {
|
|
985
|
-
const upstreamTool = {
|
|
986
|
-
name: tool.name,
|
|
987
|
-
description: tool.description ?? "",
|
|
988
|
-
inputSchema: tool.inputSchema
|
|
989
|
-
};
|
|
990
|
-
if (tool.outputSchema !== void 0) {
|
|
991
|
-
upstreamTool.outputSchema = tool.outputSchema;
|
|
992
|
-
}
|
|
993
|
-
if (tool.annotations !== void 0) {
|
|
994
|
-
upstreamTool.annotations = {
|
|
995
|
-
title: tool.annotations.title,
|
|
996
|
-
readOnlyHint: tool.annotations.readOnlyHint,
|
|
997
|
-
destructiveHint: tool.annotations.destructiveHint,
|
|
998
|
-
idempotentHint: tool.annotations.idempotentHint,
|
|
999
|
-
openWorldHint: tool.annotations.openWorldHint
|
|
1000
|
-
};
|
|
2196
|
+
static fromGroupedWithLazy(groups, lazyDescriptions) {
|
|
2197
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
2198
|
+
for (const [mcpName2, tools] of groups) {
|
|
2199
|
+
for (const tool of tools) {
|
|
2200
|
+
toolMap.set(`${mcpName2}/${tool.name}`, tool);
|
|
1001
2201
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
2202
|
+
}
|
|
2203
|
+
const description2 = buildGroupedDescription(groups, lazyDescriptions);
|
|
2204
|
+
return new _ToolCatalog(toolMap, description2);
|
|
1004
2205
|
}
|
|
1005
|
-
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
2206
|
+
getToolDetails(toolName) {
|
|
2207
|
+
const tool = this.tools.get(toolName);
|
|
2208
|
+
if (tool === void 0) {
|
|
2209
|
+
const sortedNames = [...this.tools.keys()].sort().join(", ");
|
|
2210
|
+
return `Unknown tool: "${toolName}". Available tools: ${sortedNames}`;
|
|
2211
|
+
}
|
|
2212
|
+
return buildToolDetailsString(toolName, tool);
|
|
1009
2213
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
2214
|
+
};
|
|
2215
|
+
function buildFlatDescription(tools) {
|
|
2216
|
+
const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
2217
|
+
const toolLines = sortedTools.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
|
|
2218
|
+
return `${DISCOVER_TOOL_PREAMBLE}
|
|
2219
|
+
|
|
2220
|
+
<tools>
|
|
2221
|
+
${toolLines}
|
|
2222
|
+
</tools>`;
|
|
2223
|
+
}
|
|
2224
|
+
function buildGroupedDescription(groups, lazyDescriptions) {
|
|
2225
|
+
const parts = [DISCOVER_TOOL_PREAMBLE];
|
|
2226
|
+
if (lazyDescriptions.size > 0) {
|
|
2227
|
+
parts.push(DYNAMIC_DISCOVERY_PREAMBLE);
|
|
2228
|
+
parts.push(buildMcpServersBlock(lazyDescriptions));
|
|
1014
2229
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
2230
|
+
if (groups.size > 0) {
|
|
2231
|
+
parts.push(buildToolsBlock(groups));
|
|
2232
|
+
} else if (lazyDescriptions.size > 0) {
|
|
2233
|
+
parts.push(NO_TOOLS_LOADED_FOOTER);
|
|
2234
|
+
} else {
|
|
2235
|
+
parts.push("<tools>\n</tools>");
|
|
1019
2236
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
2237
|
+
return parts.join("\n\n");
|
|
2238
|
+
}
|
|
2239
|
+
function buildToolsBlock(groups) {
|
|
2240
|
+
const sortedMcpNames = [...groups.keys()].sort();
|
|
2241
|
+
const sections = sortedMcpNames.map((mcpName2) => {
|
|
2242
|
+
const tools = groups.get(mcpName2);
|
|
2243
|
+
const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
2244
|
+
const toolLines = sortedTools.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
|
|
2245
|
+
return `${mcpName2}:
|
|
2246
|
+
${toolLines}`;
|
|
2247
|
+
});
|
|
2248
|
+
return `<tools>
|
|
2249
|
+
${sections.join("\n\n")}
|
|
2250
|
+
</tools>`;
|
|
2251
|
+
}
|
|
2252
|
+
function buildMcpServersBlock(lazyDescriptions) {
|
|
2253
|
+
const lines = [...lazyDescriptions].map(([name, desc]) => `- ${name}: ${desc}`).join("\n");
|
|
2254
|
+
return `<mcp_servers>
|
|
2255
|
+
${lines}
|
|
2256
|
+
</mcp_servers>`;
|
|
2257
|
+
}
|
|
2258
|
+
function buildToolDetailsString(displayName, tool) {
|
|
2259
|
+
const lines = [
|
|
2260
|
+
`Tool: ${displayName}`,
|
|
2261
|
+
`Description: ${tool.description}`,
|
|
2262
|
+
"",
|
|
2263
|
+
"Input Schema:",
|
|
2264
|
+
JSON.stringify(tool.inputSchema, null, 2)
|
|
2265
|
+
];
|
|
2266
|
+
if (tool.outputSchema !== void 0) {
|
|
2267
|
+
lines.push("", "Output Schema:", JSON.stringify(tool.outputSchema, null, 2));
|
|
1023
2268
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
2269
|
+
const annotationLines = buildAnnotationLines(tool);
|
|
2270
|
+
if (annotationLines.length > 0) {
|
|
2271
|
+
lines.push("", "Annotations:", ...annotationLines);
|
|
1027
2272
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
2273
|
+
return lines.join("\n");
|
|
2274
|
+
}
|
|
2275
|
+
function buildAnnotationLines(tool) {
|
|
2276
|
+
if (tool.annotations === void 0) {
|
|
2277
|
+
return [];
|
|
1031
2278
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
}
|
|
1036
|
-
await this.client.close();
|
|
1037
|
-
this.client = null;
|
|
2279
|
+
const { annotations } = tool;
|
|
2280
|
+
const lines = [];
|
|
2281
|
+
if (annotations.title !== void 0) {
|
|
2282
|
+
lines.push(`- title: ${annotations.title}`);
|
|
1038
2283
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
* the host's set of filesystem roots has changed.
|
|
1042
|
-
*/
|
|
1043
|
-
async sendRootsListChanged() {
|
|
1044
|
-
const client = this.requireClient();
|
|
1045
|
-
await client.sendRootsListChanged();
|
|
2284
|
+
if (annotations.readOnlyHint !== void 0) {
|
|
2285
|
+
lines.push(`- readOnlyHint: ${annotations.readOnlyHint}`);
|
|
1046
2286
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
client.setRequestHandler(
|
|
1050
|
-
CreateMessageRequestSchema,
|
|
1051
|
-
async (request, extra) => {
|
|
1052
|
-
return this.serverRequestHandlers.onCreateMessage(request.params, {
|
|
1053
|
-
signal: extra.signal
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
);
|
|
1057
|
-
}
|
|
1058
|
-
if (this.serverRequestHandlers.onElicitInput !== void 0) {
|
|
1059
|
-
client.setRequestHandler(
|
|
1060
|
-
ElicitRequestSchema,
|
|
1061
|
-
async (request, extra) => {
|
|
1062
|
-
return this.serverRequestHandlers.onElicitInput(request.params, {
|
|
1063
|
-
signal: extra.signal
|
|
1064
|
-
});
|
|
1065
|
-
}
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
if (this.serverRequestHandlers.onListRoots !== void 0) {
|
|
1069
|
-
client.setRequestHandler(
|
|
1070
|
-
ListRootsRequestSchema,
|
|
1071
|
-
async (request, extra) => {
|
|
1072
|
-
return this.serverRequestHandlers.onListRoots(request.params, {
|
|
1073
|
-
signal: extra.signal
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1076
|
-
);
|
|
1077
|
-
}
|
|
2287
|
+
if (annotations.destructiveHint !== void 0) {
|
|
2288
|
+
lines.push(`- destructiveHint: ${annotations.destructiveHint}`);
|
|
1078
2289
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
throw new Error("Client is not connected. Call connect() first.");
|
|
1082
|
-
}
|
|
1083
|
-
return this.client;
|
|
2290
|
+
if (annotations.idempotentHint !== void 0) {
|
|
2291
|
+
lines.push(`- idempotentHint: ${annotations.idempotentHint}`);
|
|
1084
2292
|
}
|
|
1085
|
-
|
|
2293
|
+
if (annotations.openWorldHint !== void 0) {
|
|
2294
|
+
lines.push(`- openWorldHint: ${annotations.openWorldHint}`);
|
|
2295
|
+
}
|
|
2296
|
+
return lines;
|
|
2297
|
+
}
|
|
1086
2298
|
|
|
1087
2299
|
// src/proxy/upstream-registry.ts
|
|
1088
2300
|
var UpstreamRegistry = class {
|
|
@@ -1335,6 +2547,9 @@ var Orchestrator = class {
|
|
|
1335
2547
|
}
|
|
1336
2548
|
} catch (error) {
|
|
1337
2549
|
await this.registry.deleteOne(mcpName2);
|
|
2550
|
+
if (isAuthRequiredError(error)) {
|
|
2551
|
+
throw error;
|
|
2552
|
+
}
|
|
1338
2553
|
const failures = this.lazyRegistry.recordFailure(mcpName2);
|
|
1339
2554
|
if (failures >= MAX_LOAD_ATTEMPTS) {
|
|
1340
2555
|
this.lazyRegistry.take(mcpName2);
|
|
@@ -1581,7 +2796,7 @@ var Orchestrator = class {
|
|
|
1581
2796
|
targets.push(
|
|
1582
2797
|
action(client).catch((error) => {
|
|
1583
2798
|
const message = error instanceof Error ? error.message : String(error);
|
|
1584
|
-
|
|
2799
|
+
process9.stderr.write(`dynmcp: ${label} failed for "${mcpName2}": ${message}
|
|
1585
2800
|
`);
|
|
1586
2801
|
})
|
|
1587
2802
|
);
|
|
@@ -1606,13 +2821,13 @@ function splitNamespacedName(namespacedName, knownMcpNames) {
|
|
|
1606
2821
|
}
|
|
1607
2822
|
function logCollisions(resourceRouter, promptRouter) {
|
|
1608
2823
|
for (const collision of resourceRouter.collisions()) {
|
|
1609
|
-
|
|
2824
|
+
process9.stderr.write(
|
|
1610
2825
|
`dynmcp: resource URI collision: "${collision.uri}" is provided by "${collision.chosen}" and "${collision.shadowed}"; routing to "${collision.chosen}".
|
|
1611
2826
|
`
|
|
1612
2827
|
);
|
|
1613
2828
|
}
|
|
1614
2829
|
for (const collision of promptRouter.collisions()) {
|
|
1615
|
-
|
|
2830
|
+
process9.stderr.write(
|
|
1616
2831
|
`dynmcp: prompt name collision: "${collision.name}" is provided by "${collision.chosen}" and "${collision.shadowed}"; routing to "${collision.chosen}".
|
|
1617
2832
|
`
|
|
1618
2833
|
);
|
|
@@ -1620,7 +2835,7 @@ function logCollisions(resourceRouter, promptRouter) {
|
|
|
1620
2835
|
}
|
|
1621
2836
|
|
|
1622
2837
|
// src/proxy/server.ts
|
|
1623
|
-
import
|
|
2838
|
+
import process10 from "process";
|
|
1624
2839
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1625
2840
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1626
2841
|
import {
|
|
@@ -1767,7 +2982,7 @@ var ProxyServer = class {
|
|
|
1767
2982
|
async start() {
|
|
1768
2983
|
const server = this.buildServer();
|
|
1769
2984
|
const transport = new StdioServerTransport();
|
|
1770
|
-
|
|
2985
|
+
process10.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
|
|
1771
2986
|
await server.connect(transport);
|
|
1772
2987
|
}
|
|
1773
2988
|
/**
|
|
@@ -1980,35 +3195,6 @@ var ProxyServer = class {
|
|
|
1980
3195
|
}
|
|
1981
3196
|
};
|
|
1982
3197
|
|
|
1983
|
-
// src/proxy/transport-factory.ts
|
|
1984
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
1985
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1986
|
-
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
1987
|
-
function createTransport(config) {
|
|
1988
|
-
switch (config.transport) {
|
|
1989
|
-
case "stdio":
|
|
1990
|
-
return new StdioClientTransport({
|
|
1991
|
-
command: config.command,
|
|
1992
|
-
args: config.args,
|
|
1993
|
-
env: config.env
|
|
1994
|
-
});
|
|
1995
|
-
case "streamable-http":
|
|
1996
|
-
return new StreamableHTTPClientTransport(
|
|
1997
|
-
new URL(config.url),
|
|
1998
|
-
config.headers ? { requestInit: { headers: config.headers } } : void 0
|
|
1999
|
-
);
|
|
2000
|
-
case "sse":
|
|
2001
|
-
return new SSEClientTransport(
|
|
2002
|
-
new URL(config.url),
|
|
2003
|
-
config.headers ? { requestInit: { headers: config.headers } } : void 0
|
|
2004
|
-
);
|
|
2005
|
-
default: {
|
|
2006
|
-
const _exhaustive = config;
|
|
2007
|
-
return _exhaustive;
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
3198
|
// src/proxy/index.ts
|
|
2013
3199
|
var SINGLE_MCP_NAME = "__default__";
|
|
2014
3200
|
async function startProxy(command, args) {
|
|
@@ -2026,7 +3212,7 @@ async function startProxyFromConfig(options = {}) {
|
|
|
2026
3212
|
const eagerMcps = /* @__PURE__ */ new Map();
|
|
2027
3213
|
const lazyMcps = /* @__PURE__ */ new Map();
|
|
2028
3214
|
for (const [name, entry] of Object.entries(config.mcp)) {
|
|
2029
|
-
const transport = createTransport(entry);
|
|
3215
|
+
const transport = createTransport(name, entry);
|
|
2030
3216
|
if (entry.description !== void 0) {
|
|
2031
3217
|
lazyMcps.set(name, { transport, description: entry.description });
|
|
2032
3218
|
} else {
|
|
@@ -2048,7 +3234,7 @@ function buildOrchestrator(params) {
|
|
|
2048
3234
|
lazyMcps: params.lazyMcps,
|
|
2049
3235
|
namespaced: params.namespaced,
|
|
2050
3236
|
onTransportError: (mcpName2, error) => {
|
|
2051
|
-
|
|
3237
|
+
process11.stderr.write(
|
|
2052
3238
|
`${params.transportErrorPrefix(mcpName2)} transport error: ${error.message}
|
|
2053
3239
|
`
|
|
2054
3240
|
);
|
|
@@ -2062,19 +3248,19 @@ async function runProxy(orchestrator) {
|
|
|
2062
3248
|
if (isShuttingDown) return;
|
|
2063
3249
|
isShuttingDown = true;
|
|
2064
3250
|
orchestrator.disconnectAll().catch((error) => {
|
|
2065
|
-
|
|
3251
|
+
process11.stderr.write(
|
|
2066
3252
|
`dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
|
|
2067
3253
|
`
|
|
2068
3254
|
);
|
|
2069
|
-
}).finally(() =>
|
|
3255
|
+
}).finally(() => process11.exit(exitCode));
|
|
2070
3256
|
};
|
|
2071
3257
|
activeShutdown.shutdown = shutdown;
|
|
2072
3258
|
try {
|
|
2073
3259
|
await orchestrator.connect();
|
|
2074
3260
|
} catch (error) {
|
|
2075
|
-
|
|
3261
|
+
process11.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
2076
3262
|
`);
|
|
2077
|
-
|
|
3263
|
+
process11.exit(1);
|
|
2078
3264
|
return;
|
|
2079
3265
|
}
|
|
2080
3266
|
const proxyServer = new ProxyServer({
|
|
@@ -2111,10 +3297,10 @@ async function runProxy(orchestrator) {
|
|
|
2111
3297
|
onElicitInput: (params, options) => proxyServer.forwardElicitInput(params, options),
|
|
2112
3298
|
onListRoots: (params, options) => proxyServer.forwardListRoots(params, options)
|
|
2113
3299
|
});
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
3300
|
+
process11.on("SIGINT", () => shutdown(0));
|
|
3301
|
+
process11.on("SIGTERM", () => shutdown(0));
|
|
3302
|
+
process11.stdin.on("end", () => shutdown(0));
|
|
3303
|
+
process11.stdin.on("close", () => shutdown(0));
|
|
2118
3304
|
try {
|
|
2119
3305
|
await proxyServer.start();
|
|
2120
3306
|
} catch (error) {
|
|
@@ -2133,41 +3319,98 @@ var cliBanner = chalk.bold.magentaBright(
|
|
|
2133
3319
|
);
|
|
2134
3320
|
var cli = new Command(package_default.name).description(package_default.description).version(package_default.version).addHelpText("beforeAll", cliBanner).addHelpText(
|
|
2135
3321
|
"after",
|
|
2136
|
-
"\nExamples:\n dynmcp -- npx -y chrome-devtools-mcp@latest\n dynmcp --config ./mcp.json\n"
|
|
3322
|
+
"\nExamples:\n dynmcp -- npx -y chrome-devtools-mcp@latest\n dynmcp --config ./mcp.json\n dynmcp ls\n dynmcp test github\n dynmcp login github\n dynmcp logout github\n"
|
|
2137
3323
|
).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) => {
|
|
2138
|
-
const separatorIndex =
|
|
3324
|
+
const separatorIndex = process12.argv.indexOf("--");
|
|
2139
3325
|
const configPath = cmd.opts().config;
|
|
2140
3326
|
const envFilePath = cmd.opts().env;
|
|
2141
3327
|
if (separatorIndex !== -1) {
|
|
2142
|
-
const [command, ...args] =
|
|
3328
|
+
const [command, ...args] = process12.argv.slice(separatorIndex + 1);
|
|
2143
3329
|
if (command === void 0) {
|
|
2144
|
-
|
|
3330
|
+
process12.stderr.write(
|
|
2145
3331
|
"dynmcp: no upstream command provided after --.\nUsage: dynmcp -- <command> [args...]\n"
|
|
2146
3332
|
);
|
|
2147
|
-
|
|
3333
|
+
process12.exit(1);
|
|
2148
3334
|
}
|
|
2149
3335
|
try {
|
|
2150
3336
|
await startProxy(command, args);
|
|
2151
3337
|
} catch (error) {
|
|
2152
|
-
|
|
3338
|
+
process12.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
2153
3339
|
`);
|
|
2154
|
-
|
|
3340
|
+
process12.exit(1);
|
|
2155
3341
|
}
|
|
2156
3342
|
return;
|
|
2157
3343
|
}
|
|
2158
3344
|
try {
|
|
2159
3345
|
await startProxyFromConfig({ configPath, envFilePath });
|
|
2160
3346
|
} catch (error) {
|
|
2161
|
-
|
|
3347
|
+
process12.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
3348
|
+
`);
|
|
3349
|
+
process12.exit(1);
|
|
3350
|
+
}
|
|
3351
|
+
});
|
|
3352
|
+
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) => {
|
|
3353
|
+
try {
|
|
3354
|
+
await login({
|
|
3355
|
+
mcpName: name,
|
|
3356
|
+
...options.config !== void 0 ? { configPath: options.config } : {},
|
|
3357
|
+
...options.env !== void 0 ? { envFilePath: options.env } : {}
|
|
3358
|
+
});
|
|
3359
|
+
} catch (error) {
|
|
3360
|
+
process12.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
3361
|
+
`);
|
|
3362
|
+
process12.exit(1);
|
|
3363
|
+
}
|
|
3364
|
+
});
|
|
3365
|
+
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) => {
|
|
3366
|
+
try {
|
|
3367
|
+
await logout({
|
|
3368
|
+
mcpName: name,
|
|
3369
|
+
...options.config !== void 0 ? { configPath: options.config } : {},
|
|
3370
|
+
...options.env !== void 0 ? { envFilePath: options.env } : {}
|
|
3371
|
+
});
|
|
3372
|
+
} catch (error) {
|
|
3373
|
+
process12.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
3374
|
+
`);
|
|
3375
|
+
process12.exit(1);
|
|
3376
|
+
}
|
|
3377
|
+
});
|
|
3378
|
+
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) => {
|
|
3379
|
+
try {
|
|
3380
|
+
await list({
|
|
3381
|
+
...options.config !== void 0 ? { configPath: options.config } : {},
|
|
3382
|
+
...options.env !== void 0 ? { envFilePath: options.env } : {},
|
|
3383
|
+
...options.json === true ? { json: true } : {}
|
|
3384
|
+
});
|
|
3385
|
+
} catch (error) {
|
|
3386
|
+
process12.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
2162
3387
|
`);
|
|
2163
|
-
|
|
3388
|
+
process12.exit(1);
|
|
2164
3389
|
}
|
|
2165
3390
|
});
|
|
3391
|
+
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(
|
|
3392
|
+
async (name, options) => {
|
|
3393
|
+
try {
|
|
3394
|
+
const exitCode = await test({
|
|
3395
|
+
...name !== void 0 ? { mcpName: name } : {},
|
|
3396
|
+
...options.config !== void 0 ? { configPath: options.config } : {},
|
|
3397
|
+
...options.env !== void 0 ? { envFilePath: options.env } : {},
|
|
3398
|
+
...options.json === true ? { json: true } : {},
|
|
3399
|
+
...options.timeout !== void 0 && !Number.isNaN(options.timeout) ? { timeoutMs: options.timeout } : {}
|
|
3400
|
+
});
|
|
3401
|
+
if (exitCode !== 0) process12.exit(exitCode);
|
|
3402
|
+
} catch (error) {
|
|
3403
|
+
process12.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
|
|
3404
|
+
`);
|
|
3405
|
+
process12.exit(1);
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
);
|
|
2166
3409
|
|
|
2167
3410
|
// src/index.ts
|
|
2168
|
-
import
|
|
3411
|
+
import process13 from "process";
|
|
2169
3412
|
async function main() {
|
|
2170
|
-
cli.parse(
|
|
3413
|
+
cli.parse(process13.argv);
|
|
2171
3414
|
}
|
|
2172
3415
|
main();
|
|
2173
3416
|
//# sourceMappingURL=index.js.map
|