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/dist/index.cjs CHANGED
@@ -23,18 +23,18 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
 
25
25
  // src/cli.ts
26
- var import_node_process7 = __toESM(require("process"), 1);
26
+ var import_node_process12 = __toESM(require("process"), 1);
27
27
  var import_commander = require("commander");
28
28
 
29
29
  // package.json
30
30
  var package_default = {
31
31
  name: "dynmcp",
32
- version: "0.4.0",
32
+ version: "0.5.0",
33
33
  description: "Dynamic MCP context management tool for AI MCP-enabled agents and clients.",
34
34
  author: "Brandon Burrus <brandon@burrus.io>",
35
35
  license: "MIT",
36
36
  type: "module",
37
- homepage: "https://github.com/brandonburrus/dynamic-discovery-mcp#readme",
37
+ homepage: "https://dynamicmcp.tools",
38
38
  keywords: [
39
39
  "mcp",
40
40
  "model-context-protocol",
@@ -74,12 +74,10 @@ var package_default = {
74
74
  }
75
75
  },
76
76
  files: [
77
- "dist",
78
- "schema"
77
+ "dist"
79
78
  ],
80
79
  scripts: {
81
80
  "generate:schema": "tsx scripts/generate-schema.ts",
82
- prebuild: "tsx scripts/generate-schema.ts",
83
81
  build: "tsup",
84
82
  dev: "tsx src/index.ts",
85
83
  typecheck: "tsc --noEmit",
@@ -93,6 +91,7 @@ var package_default = {
93
91
  },
94
92
  dependencies: {
95
93
  "@modelcontextprotocol/sdk": "^1.29.0",
94
+ "@napi-rs/keyring": "^1.3.0",
96
95
  boxen: "^8.0.1",
97
96
  chalk: "^5.6.2",
98
97
  commander: "^14.0.3",
@@ -122,9 +121,330 @@ var package_default = {
122
121
  var import_figlet = __toESM(require("figlet"), 1);
123
122
  var import_chalk = __toESM(require("chalk"), 1);
124
123
 
125
- // src/proxy/index.ts
126
- var import_node_process6 = __toESM(require("process"), 1);
127
- var import_stdio3 = require("@modelcontextprotocol/sdk/client/stdio.js");
124
+ // src/auth/errors.ts
125
+ var AuthRequiredError = class extends Error {
126
+ mcpName;
127
+ constructor(mcpName2) {
128
+ super(
129
+ `Upstream MCP "${mcpName2}" requires authorization. Run \`dynmcp login ${mcpName2}\` from your terminal, then retry.`
130
+ );
131
+ this.name = "AuthRequiredError";
132
+ this.mcpName = mcpName2;
133
+ }
134
+ };
135
+ function isAuthRequiredError(error) {
136
+ let current = error;
137
+ for (let depth = 0; depth < 8 && current !== null && current !== void 0; depth += 1) {
138
+ if (current instanceof AuthRequiredError) return true;
139
+ if (current instanceof Error && current.name === "AuthRequiredError") return true;
140
+ if (current instanceof Error) {
141
+ current = current.cause;
142
+ continue;
143
+ }
144
+ return false;
145
+ }
146
+ return false;
147
+ }
148
+
149
+ // src/auth/keychain-store.ts
150
+ var import_keyring = require("@napi-rs/keyring");
151
+
152
+ // src/auth/types.ts
153
+ var KEYCHAIN_BLOB_VERSION = 1;
154
+
155
+ // src/auth/keychain-store.ts
156
+ var KEYCHAIN_SERVICE = "dynmcp";
157
+ function buildKeychainAccount(mcpName2, serverUrl) {
158
+ const origin = new URL(serverUrl).origin;
159
+ return `${mcpName2}:${origin}`;
160
+ }
161
+ var KeychainStore = class {
162
+ constructor(mcpName2, serverUrl, service = KEYCHAIN_SERVICE) {
163
+ this.mcpName = mcpName2;
164
+ this.serverUrl = serverUrl;
165
+ this.entry = new import_keyring.Entry(service, buildKeychainAccount(mcpName2, serverUrl));
166
+ }
167
+ mcpName;
168
+ serverUrl;
169
+ entry;
170
+ /**
171
+ * Returns the parsed blob or `undefined` if no entry exists. Entries written under
172
+ * a different {@link KeychainBlob.version} are treated as absent — the caller must
173
+ * re-authenticate. Malformed JSON also returns `undefined` (corrupt entries are not
174
+ * surfaced as errors; recovery is the same: re-auth).
175
+ */
176
+ get() {
177
+ const raw = this.entry.getPassword();
178
+ if (raw === null) return void 0;
179
+ let parsed;
180
+ try {
181
+ parsed = JSON.parse(raw);
182
+ } catch {
183
+ return void 0;
184
+ }
185
+ if (!isCurrentVersionBlob(parsed)) {
186
+ return void 0;
187
+ }
188
+ return parsed;
189
+ }
190
+ /**
191
+ * Persists the blob atomically. Caller must construct a complete {@link
192
+ * KeychainBlob} — partial updates are not supported. To mutate, call {@link get},
193
+ * spread, and pass the result back to {@link set}.
194
+ */
195
+ set(blob) {
196
+ const stamped = { ...blob, version: KEYCHAIN_BLOB_VERSION };
197
+ this.entry.setPassword(JSON.stringify(stamped));
198
+ }
199
+ /**
200
+ * Deletes the entry. Returns `true` if an entry was present and removed, `false`
201
+ * if there was nothing to delete. Idempotent: callers should treat both outcomes
202
+ * as success (a no-op delete is not an error).
203
+ */
204
+ delete() {
205
+ return this.entry.deletePassword();
206
+ }
207
+ };
208
+ function isCurrentVersionBlob(value) {
209
+ return typeof value === "object" && value !== null && "version" in value && value.version === KEYCHAIN_BLOB_VERSION;
210
+ }
211
+
212
+ // src/auth/oauth-provider.ts
213
+ var import_node_crypto = require("crypto");
214
+ var CLIENT_NAME = "dynmcp";
215
+ var SOFTWARE_ID = "dynmcp";
216
+ var REFRESH_SLACK_SECONDS = 30;
217
+ var BaseOAuthProvider = class {
218
+ constructor(mcpName2, keychain, configAuth) {
219
+ this.mcpName = mcpName2;
220
+ this.keychain = keychain;
221
+ this.configAuth = configAuth;
222
+ }
223
+ mcpName;
224
+ keychain;
225
+ configAuth;
226
+ clientInformation() {
227
+ if (this.configAuth !== void 0) {
228
+ const info2 = { client_id: this.configAuth.client_id };
229
+ if (this.configAuth.client_secret !== void 0) {
230
+ info2.client_secret = this.configAuth.client_secret;
231
+ }
232
+ return info2;
233
+ }
234
+ const blob = this.keychain.get();
235
+ if (blob?.dcr === void 0) return void 0;
236
+ const info = { client_id: blob.dcr.client_id };
237
+ if (blob.dcr.client_secret !== void 0) {
238
+ info.client_secret = blob.dcr.client_secret;
239
+ }
240
+ return info;
241
+ }
242
+ tokens() {
243
+ const blob = this.keychain.get();
244
+ if (blob === void 0) return void 0;
245
+ const remaining = Math.max(
246
+ 0,
247
+ blob.expires_at - Math.floor(Date.now() / 1e3) - REFRESH_SLACK_SECONDS
248
+ );
249
+ const tokens = {
250
+ access_token: blob.access_token,
251
+ token_type: blob.token_type,
252
+ expires_in: remaining
253
+ };
254
+ if (blob.refresh_token !== void 0) tokens.refresh_token = blob.refresh_token;
255
+ if (blob.scope_granted !== void 0) tokens.scope = blob.scope_granted;
256
+ return tokens;
257
+ }
258
+ /**
259
+ * Builds the {@link OAuthDiscoveryState} the SDK can use to skip rediscovery,
260
+ * reconstructed from the cached keychain blob. Returns `undefined` when there is
261
+ * no cached blob (e.g. fresh login flow before saveTokens fires).
262
+ */
263
+ buildDiscoveryStateFromBlob(blob) {
264
+ if (blob === void 0) return void 0;
265
+ return {
266
+ authorizationServerUrl: blob.authorization_server.issuer,
267
+ authorizationServerMetadata: {
268
+ issuer: blob.authorization_server.issuer,
269
+ authorization_endpoint: blob.authorization_server.authorization_endpoint,
270
+ token_endpoint: blob.authorization_server.token_endpoint,
271
+ ...blob.authorization_server.registration_endpoint !== void 0 ? { registration_endpoint: blob.authorization_server.registration_endpoint } : {},
272
+ response_types_supported: ["code"]
273
+ },
274
+ resourceMetadata: {
275
+ resource: blob.resource_metadata.resource,
276
+ authorization_servers: blob.resource_metadata.authorization_servers
277
+ }
278
+ };
279
+ }
280
+ };
281
+ var ProxyOAuthProvider = class extends BaseOAuthProvider {
282
+ get redirectUrl() {
283
+ return void 0;
284
+ }
285
+ get clientMetadata() {
286
+ return {
287
+ client_name: CLIENT_NAME,
288
+ software_id: SOFTWARE_ID,
289
+ redirect_uris: [],
290
+ grant_types: ["authorization_code", "refresh_token"],
291
+ response_types: ["code"],
292
+ token_endpoint_auth_method: this.configAuth?.client_secret !== void 0 ? "client_secret_basic" : "none",
293
+ ...this.configAuth?.scope !== void 0 ? { scope: this.configAuth.scope } : {}
294
+ };
295
+ }
296
+ saveTokens(tokens) {
297
+ const existing = this.keychain.get();
298
+ if (existing === void 0) {
299
+ throw new AuthRequiredError(this.mcpName);
300
+ }
301
+ const expiresAt = tokens.expires_in !== void 0 ? Math.floor(Date.now() / 1e3) + tokens.expires_in : existing.expires_at;
302
+ const updated = {
303
+ ...existing,
304
+ access_token: tokens.access_token,
305
+ token_type: tokens.token_type ?? "Bearer",
306
+ expires_at: expiresAt,
307
+ refresh_token: tokens.refresh_token ?? existing.refresh_token,
308
+ ...tokens.scope !== void 0 ? { scope_granted: tokens.scope } : {}
309
+ };
310
+ this.keychain.set(updated);
311
+ }
312
+ redirectToAuthorization(_url) {
313
+ throw new AuthRequiredError(this.mcpName);
314
+ }
315
+ saveCodeVerifier(_verifier) {
316
+ throw new AuthRequiredError(this.mcpName);
317
+ }
318
+ codeVerifier() {
319
+ throw new AuthRequiredError(this.mcpName);
320
+ }
321
+ discoveryState() {
322
+ return this.buildDiscoveryStateFromBlob(this.keychain.get());
323
+ }
324
+ invalidateCredentials(_scope) {
325
+ this.keychain.delete();
326
+ }
327
+ };
328
+ var LoginOAuthProvider = class extends BaseOAuthProvider {
329
+ redirectUriString;
330
+ pending = {};
331
+ callbacks;
332
+ constructor(opts) {
333
+ super(opts.mcpName, opts.keychain, opts.configAuth);
334
+ this.redirectUriString = opts.redirectUri;
335
+ this.callbacks = opts.callbacks;
336
+ }
337
+ get redirectUrl() {
338
+ return this.redirectUriString;
339
+ }
340
+ get clientMetadata() {
341
+ return {
342
+ client_name: CLIENT_NAME,
343
+ software_id: SOFTWARE_ID,
344
+ redirect_uris: [this.redirectUriString],
345
+ grant_types: ["authorization_code", "refresh_token"],
346
+ response_types: ["code"],
347
+ token_endpoint_auth_method: this.configAuth?.client_secret !== void 0 ? "client_secret_basic" : "none",
348
+ ...this.configAuth?.scope !== void 0 ? { scope: this.configAuth.scope } : {}
349
+ };
350
+ }
351
+ state() {
352
+ if (this.pending.state !== void 0) return this.pending.state;
353
+ const generated = (0, import_node_crypto.randomBytes)(32).toString("base64url");
354
+ this.pending.state = generated;
355
+ return generated;
356
+ }
357
+ /** The state value generated for this flow, for the callback handler to verify. */
358
+ get currentState() {
359
+ return this.pending.state;
360
+ }
361
+ clientInformation() {
362
+ if (this.pending.dcr !== void 0) {
363
+ const info = { client_id: this.pending.dcr.client_id };
364
+ if (this.pending.dcr.client_secret !== void 0) {
365
+ info.client_secret = this.pending.dcr.client_secret;
366
+ }
367
+ return info;
368
+ }
369
+ return super.clientInformation();
370
+ }
371
+ saveClientInformation(info) {
372
+ this.pending.dcr = info;
373
+ }
374
+ saveTokens(tokens) {
375
+ const discovery = this.pending.discovery ?? this.buildDiscoveryStateFromBlob(this.keychain.get());
376
+ if (discovery === void 0) {
377
+ throw new Error(
378
+ `Cannot persist tokens for "${this.mcpName}": no discovery state captured during the flow.`
379
+ );
380
+ }
381
+ if (discovery.authorizationServerMetadata === void 0) {
382
+ throw new Error(
383
+ `Cannot persist tokens for "${this.mcpName}": authorization server metadata not available.`
384
+ );
385
+ }
386
+ if (discovery.resourceMetadata === void 0) {
387
+ throw new Error(
388
+ `Cannot persist tokens for "${this.mcpName}": protected resource metadata not available.`
389
+ );
390
+ }
391
+ const expiresAt = tokens.expires_in !== void 0 ? Math.floor(Date.now() / 1e3) + tokens.expires_in : Math.floor(Date.now() / 1e3) + 3600;
392
+ const authorizationServer = {
393
+ issuer: discovery.authorizationServerMetadata.issuer ?? discovery.authorizationServerUrl,
394
+ authorization_endpoint: discovery.authorizationServerMetadata.authorization_endpoint,
395
+ token_endpoint: discovery.authorizationServerMetadata.token_endpoint,
396
+ ...discovery.authorizationServerMetadata.registration_endpoint !== void 0 ? { registration_endpoint: discovery.authorizationServerMetadata.registration_endpoint } : {}
397
+ };
398
+ const resourceMetadata = {
399
+ resource: discovery.resourceMetadata.resource,
400
+ authorization_servers: discovery.resourceMetadata.authorization_servers ?? []
401
+ };
402
+ const blob = {
403
+ version: KEYCHAIN_BLOB_VERSION,
404
+ access_token: tokens.access_token,
405
+ token_type: tokens.token_type ?? "Bearer",
406
+ expires_at: expiresAt,
407
+ ...tokens.refresh_token !== void 0 ? { refresh_token: tokens.refresh_token } : {},
408
+ ...tokens.scope !== void 0 ? { scope_granted: tokens.scope } : {},
409
+ authorization_server: authorizationServer,
410
+ resource_metadata: resourceMetadata,
411
+ ...this.pending.dcr !== void 0 ? {
412
+ dcr: {
413
+ client_id: this.pending.dcr.client_id,
414
+ ...this.pending.dcr.client_secret !== void 0 ? { client_secret: this.pending.dcr.client_secret } : {}
415
+ }
416
+ } : {}
417
+ };
418
+ this.keychain.set(blob);
419
+ }
420
+ async redirectToAuthorization(url) {
421
+ await this.callbacks.onAuthorizationUrl(url);
422
+ }
423
+ saveCodeVerifier(verifier) {
424
+ this.pending.codeVerifier = verifier;
425
+ }
426
+ codeVerifier() {
427
+ if (this.pending.codeVerifier === void 0) {
428
+ throw new Error("Code verifier requested before it was saved.");
429
+ }
430
+ return this.pending.codeVerifier;
431
+ }
432
+ saveDiscoveryState(state) {
433
+ this.pending.discovery = state;
434
+ }
435
+ /**
436
+ * Force a fresh RFC 9728 + RFC 8414 discovery for every login flow. We intentionally
437
+ * do NOT pre-seed from the keychain on login — if endpoints changed since the last
438
+ * login, we want to pick them up now and persist the new snapshot.
439
+ */
440
+ discoveryState() {
441
+ return void 0;
442
+ }
443
+ };
444
+
445
+ // src/auth/login.ts
446
+ var import_node_process4 = __toESM(require("process"), 1);
447
+ var import_auth = require("@modelcontextprotocol/sdk/client/auth.js");
128
448
 
129
449
  // src/config/schema.ts
130
450
  var import_zod = require("zod");
@@ -146,17 +466,26 @@ var stdioTransport = import_zod.z.object({
146
466
  var httpUrl = import_zod.z.string().url().refine((u) => u.startsWith("http://") || u.startsWith("https://"), {
147
467
  message: "URL must use http:// or https:// scheme"
148
468
  });
469
+ var authConfig = import_zod.z.object({
470
+ client_id: import_zod.z.string().min(1, { message: "auth.client_id must be a non-empty string" }).refine((value) => value.trim().length > 0, {
471
+ message: "auth.client_id must not be whitespace-only"
472
+ }),
473
+ client_secret: import_zod.z.string().min(1).optional(),
474
+ scope: import_zod.z.string().min(1).optional()
475
+ }).strict().optional();
149
476
  var streamableHttpTransport = import_zod.z.object({
150
477
  transport: import_zod.z.literal("streamable-http"),
151
478
  description,
152
479
  url: httpUrl,
153
- headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
480
+ headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
481
+ auth: authConfig
154
482
  }).strict();
155
483
  var sseTransport = import_zod.z.object({
156
484
  transport: import_zod.z.literal("sse"),
157
485
  description,
158
486
  url: httpUrl,
159
- headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
487
+ headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
488
+ auth: authConfig
160
489
  }).strict();
161
490
  var transportConfig = import_zod.z.discriminatedUnion("transport", [
162
491
  stdioTransport,
@@ -232,9 +561,9 @@ function filterDefined(env) {
232
561
  var TOP_LEVEL_PASSTHROUGH_KEYS = /* @__PURE__ */ new Set(["$schema", "env"]);
233
562
  var MissingEnvVarsError = class extends Error {
234
563
  constructor(missingVars) {
235
- const list = missingVars.join(", ");
564
+ const list2 = missingVars.join(", ");
236
565
  const plural = missingVars.length === 1 ? "" : "s";
237
- super(`Missing required environment variable${plural}: ${list}`);
566
+ super(`Missing required environment variable${plural}: ${list2}`);
238
567
  this.missingVars = missingVars;
239
568
  this.name = "MissingEnvVarsError";
240
569
  }
@@ -383,17 +712,1104 @@ function readEnvMode(content) {
383
712
  if (typeof value === "string" && VALID_ENV_MODES.includes(value)) {
384
713
  return value;
385
714
  }
386
- return DEFAULT_ENV_MODE;
387
- }
388
- function isYamlFile(filePath) {
389
- return filePath.endsWith(".yml") || filePath.endsWith(".yaml");
715
+ return DEFAULT_ENV_MODE;
716
+ }
717
+ function isYamlFile(filePath) {
718
+ return filePath.endsWith(".yml") || filePath.endsWith(".yaml");
719
+ }
720
+
721
+ // src/config/json-schema.ts
722
+ var import_zod2 = require("zod");
723
+
724
+ // src/auth/browser.ts
725
+ var import_node_child_process = require("child_process");
726
+ var import_node_process3 = __toESM(require("process"), 1);
727
+ async function openUrl(url) {
728
+ const { command, args } = openerForPlatform(url);
729
+ return new Promise((resolve3, reject) => {
730
+ const child = (0, import_node_child_process.spawn)(command, args, {
731
+ stdio: "ignore",
732
+ detached: true
733
+ });
734
+ child.once("error", reject);
735
+ child.once("spawn", () => {
736
+ child.unref();
737
+ resolve3();
738
+ });
739
+ });
740
+ }
741
+ function openerForPlatform(url) {
742
+ switch (import_node_process3.default.platform) {
743
+ case "darwin":
744
+ return { command: "open", args: [url] };
745
+ case "win32":
746
+ return { command: "cmd", args: ["/c", "start", '""', url] };
747
+ default:
748
+ return { command: "xdg-open", args: [url] };
749
+ }
750
+ }
751
+
752
+ // src/auth/callback-server.ts
753
+ var import_node_http = require("http");
754
+ var CallbackTimeoutError = class extends Error {
755
+ constructor(timeoutMs) {
756
+ super(`Timed out after ${timeoutMs}ms waiting for the OAuth callback.`);
757
+ this.name = "CallbackTimeoutError";
758
+ }
759
+ };
760
+ var CallbackOAuthError = class extends Error {
761
+ constructor(oauthError, oauthErrorDescription) {
762
+ super(
763
+ oauthErrorDescription ? `OAuth error from authorization server: ${oauthError} \u2014 ${oauthErrorDescription}` : `OAuth error from authorization server: ${oauthError}`
764
+ );
765
+ this.oauthError = oauthError;
766
+ this.oauthErrorDescription = oauthErrorDescription;
767
+ this.name = "CallbackOAuthError";
768
+ }
769
+ oauthError;
770
+ oauthErrorDescription;
771
+ };
772
+ var SUCCESS_HTML = `<!DOCTYPE html>
773
+ <html lang="en">
774
+ <head>
775
+ <meta charset="utf-8" />
776
+ <title>dynmcp \u2014 authorization complete</title>
777
+ <style>
778
+ body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 4rem; max-width: 36rem; margin: 0 auto; color: #222; }
779
+ code { background: #f4f4f4; padding: 0.1rem 0.3rem; border-radius: 3px; }
780
+ </style>
781
+ </head>
782
+ <body>
783
+ <h1>Authorization complete</h1>
784
+ <p>You may close this tab and return to your terminal.</p>
785
+ <p><small>Issued by <code>dynmcp</code>.</small></p>
786
+ </body>
787
+ </html>
788
+ `;
789
+ var ERROR_HTML_PREFIX = `<!DOCTYPE html>
790
+ <html lang="en">
791
+ <head><meta charset="utf-8" /><title>dynmcp \u2014 authorization failed</title></head>
792
+ <body style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 4rem; max-width: 36rem; margin: 0 auto;">
793
+ <h1>Authorization failed</h1>
794
+ <p>`;
795
+ var ERROR_HTML_SUFFIX = `</p>
796
+ <p>Return to your terminal for details.</p>
797
+ </body>
798
+ </html>
799
+ `;
800
+ var CallbackServer = class _CallbackServer {
801
+ server = null;
802
+ boundPort = null;
803
+ pending = null;
804
+ /** The redirect path served. Must match the redirect URI registered with the OAuth client. */
805
+ static CALLBACK_PATH = "/callback";
806
+ /** Begins listening on `127.0.0.1` at an OS-assigned port. */
807
+ async start() {
808
+ if (this.server !== null) {
809
+ throw new Error("CallbackServer is already started.");
810
+ }
811
+ const server = (0, import_node_http.createServer)((req, res) => this.handleRequest(req, res));
812
+ await new Promise((resolve3, reject) => {
813
+ const onError = (err) => {
814
+ server.removeListener("listening", onListening);
815
+ reject(err);
816
+ };
817
+ const onListening = () => {
818
+ server.removeListener("error", onError);
819
+ resolve3();
820
+ };
821
+ server.once("error", onError);
822
+ server.once("listening", onListening);
823
+ server.listen({ port: 0, host: "127.0.0.1" });
824
+ });
825
+ const address = server.address();
826
+ if (address === null || typeof address === "string") {
827
+ server.close();
828
+ throw new Error("Failed to determine bound port for callback server.");
829
+ }
830
+ this.boundPort = address.port;
831
+ this.server = server;
832
+ }
833
+ /** The port the OS assigned. Available after {@link start} resolves. */
834
+ get port() {
835
+ if (this.boundPort === null) {
836
+ throw new Error("CallbackServer is not started.");
837
+ }
838
+ return this.boundPort;
839
+ }
840
+ /** The full redirect URI to register with the authorization server. */
841
+ get redirectUri() {
842
+ return `http://127.0.0.1:${this.port}${_CallbackServer.CALLBACK_PATH}`;
843
+ }
844
+ /**
845
+ * Resolves with the captured `code` and `state` once a valid callback is received,
846
+ * or rejects with {@link CallbackTimeoutError} / {@link CallbackOAuthError} on the
847
+ * documented failure paths. Only one callback is accepted; subsequent requests to
848
+ * `/callback` after the first valid hit return `400`.
849
+ */
850
+ awaitCallback(timeoutMs) {
851
+ if (this.server === null) {
852
+ return Promise.reject(new Error("CallbackServer is not started."));
853
+ }
854
+ if (this.pending !== null) {
855
+ return Promise.reject(new Error("awaitCallback already in progress."));
856
+ }
857
+ return new Promise((resolve3, reject) => {
858
+ const timer = setTimeout(() => {
859
+ this.pending = null;
860
+ reject(new CallbackTimeoutError(timeoutMs));
861
+ }, timeoutMs);
862
+ this.pending = { resolve: resolve3, reject, timer };
863
+ });
864
+ }
865
+ /** Closes the listening socket. Safe to call multiple times. */
866
+ async stop() {
867
+ if (this.pending !== null) {
868
+ clearTimeout(this.pending.timer);
869
+ this.pending = null;
870
+ }
871
+ if (this.server === null) return;
872
+ await new Promise((resolve3) => {
873
+ this.server.close(() => resolve3());
874
+ });
875
+ this.server = null;
876
+ this.boundPort = null;
877
+ }
878
+ handleRequest(req, res) {
879
+ if (req.method !== "GET") {
880
+ res.writeHead(405, { "content-type": "text/plain", allow: "GET" });
881
+ res.end("Method Not Allowed");
882
+ return;
883
+ }
884
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${this.boundPort ?? 0}`);
885
+ if (url.pathname !== _CallbackServer.CALLBACK_PATH) {
886
+ res.writeHead(404, { "content-type": "text/plain" });
887
+ res.end("Not Found");
888
+ return;
889
+ }
890
+ if (this.pending === null) {
891
+ res.writeHead(400, { "content-type": "text/plain" });
892
+ res.end("No callback expected.");
893
+ return;
894
+ }
895
+ const oauthError = url.searchParams.get("error");
896
+ if (oauthError !== null) {
897
+ const errorDescription = url.searchParams.get("error_description") ?? void 0;
898
+ this.respondError(res, oauthError, errorDescription);
899
+ const { reject, timer: timer2 } = this.pending;
900
+ clearTimeout(timer2);
901
+ this.pending = null;
902
+ reject(new CallbackOAuthError(oauthError, errorDescription));
903
+ return;
904
+ }
905
+ const code = url.searchParams.get("code");
906
+ const state = url.searchParams.get("state");
907
+ if (code === null || state === null) {
908
+ this.respondError(res, "invalid_callback", "Missing code or state parameter.");
909
+ const { reject, timer: timer2 } = this.pending;
910
+ clearTimeout(timer2);
911
+ this.pending = null;
912
+ reject(new Error("Callback missing code or state parameter."));
913
+ return;
914
+ }
915
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
916
+ res.end(SUCCESS_HTML);
917
+ const { resolve: resolve3, timer } = this.pending;
918
+ clearTimeout(timer);
919
+ this.pending = null;
920
+ resolve3({ code, state });
921
+ }
922
+ respondError(res, errorCode, description2) {
923
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
924
+ res.end(
925
+ ERROR_HTML_PREFIX + escapeHtml(description2 ?? `OAuth error: ${errorCode}`) + ERROR_HTML_SUFFIX
926
+ );
927
+ }
928
+ };
929
+ function escapeHtml(value) {
930
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
931
+ }
932
+
933
+ // src/auth/login.ts
934
+ var CALLBACK_TIMEOUT_MS = 6e4;
935
+ async function login(options) {
936
+ const config = loadConfig({
937
+ configPath: options.configPath,
938
+ envFilePath: options.envFilePath
939
+ });
940
+ const entry = resolveOAuthCapableEntry(config, options.mcpName);
941
+ const writeStatus = options.writeStatus ?? defaultStatusWriter;
942
+ const openInBrowser = options.openInBrowser ?? openUrl;
943
+ writeStatus(`Probing ${entry.url} for OAuth challenge...
944
+ `);
945
+ const resourceMetadataUrl = await probeFor401ResourceMetadata(entry.url);
946
+ if (resourceMetadataUrl === void 0) {
947
+ throw new Error(
948
+ `Upstream "${options.mcpName}" did not return a 401 challenge with a WWW-Authenticate \`resource_metadata\` URL. The server does not appear to require OAuth; no credentials stored.`
949
+ );
950
+ }
951
+ const keychain = new KeychainStore(options.mcpName, entry.url);
952
+ const callbackServer = new CallbackServer();
953
+ await callbackServer.start();
954
+ writeStatus(`Callback server listening on ${callbackServer.redirectUri}
955
+ `);
956
+ try {
957
+ const provider = new LoginOAuthProvider({
958
+ mcpName: options.mcpName,
959
+ keychain,
960
+ configAuth: configAuthFromEntry(entry),
961
+ redirectUri: callbackServer.redirectUri,
962
+ callbacks: {
963
+ onAuthorizationUrl: async (url) => {
964
+ writeStatus(`Opening browser for authorization: ${url.toString()}
965
+ `);
966
+ try {
967
+ await openInBrowser(url.toString());
968
+ } catch (error) {
969
+ const reason = error instanceof Error ? error.message : String(error);
970
+ writeStatus(
971
+ `Failed to launch browser (${reason}). Open this URL manually:
972
+ ${url.toString()}
973
+ `
974
+ );
975
+ }
976
+ }
977
+ }
978
+ });
979
+ const firstResult = await (0, import_auth.auth)(provider, {
980
+ serverUrl: entry.url,
981
+ resourceMetadataUrl,
982
+ ...entry.auth?.scope !== void 0 ? { scope: entry.auth.scope } : {}
983
+ });
984
+ if (firstResult === "AUTHORIZED") {
985
+ writeStatus(`Already authorized for "${options.mcpName}"; no changes made.
986
+ `);
987
+ return;
988
+ }
989
+ writeStatus(`Waiting for browser callback (timeout ${CALLBACK_TIMEOUT_MS / 1e3}s)...
990
+ `);
991
+ const { code, state: receivedState } = await callbackServer.awaitCallback(CALLBACK_TIMEOUT_MS);
992
+ const expectedState = provider.currentState;
993
+ if (expectedState === void 0 || receivedState !== expectedState) {
994
+ throw new Error(
995
+ "OAuth state mismatch on callback. Possible CSRF attempt or stale browser tab; not exchanging the authorization code."
996
+ );
997
+ }
998
+ const secondResult = await (0, import_auth.auth)(provider, {
999
+ serverUrl: entry.url,
1000
+ authorizationCode: code,
1001
+ resourceMetadataUrl,
1002
+ ...entry.auth?.scope !== void 0 ? { scope: entry.auth.scope } : {}
1003
+ });
1004
+ if (secondResult !== "AUTHORIZED") {
1005
+ throw new Error(`Token exchange did not return AUTHORIZED (got ${secondResult}).`);
1006
+ }
1007
+ writeStatus(`Successfully authenticated "${options.mcpName}".
1008
+ `);
1009
+ } finally {
1010
+ await callbackServer.stop();
1011
+ }
1012
+ }
1013
+ function resolveOAuthCapableEntry(config, mcpName2) {
1014
+ const entry = config.mcp[mcpName2];
1015
+ if (entry === void 0) {
1016
+ const available = Object.keys(config.mcp).sort().join(", ");
1017
+ throw new Error(`Unknown MCP "${mcpName2}". Configured MCPs: ${available || "(none)"}.`);
1018
+ }
1019
+ if (entry.transport !== "streamable-http" && entry.transport !== "sse") {
1020
+ throw new Error(
1021
+ `MCP "${mcpName2}" uses the "${entry.transport}" transport; OAuth is only supported for streamable-http and sse upstreams.`
1022
+ );
1023
+ }
1024
+ return entry;
1025
+ }
1026
+ function configAuthFromEntry(entry) {
1027
+ if (entry.auth === void 0) return void 0;
1028
+ const overrides = { client_id: entry.auth.client_id };
1029
+ if (entry.auth.client_secret !== void 0) overrides.client_secret = entry.auth.client_secret;
1030
+ if (entry.auth.scope !== void 0) overrides.scope = entry.auth.scope;
1031
+ return overrides;
1032
+ }
1033
+ async function probeFor401ResourceMetadata(serverUrl) {
1034
+ let response;
1035
+ try {
1036
+ response = await fetch(serverUrl, { method: "GET", redirect: "manual" });
1037
+ } catch (error) {
1038
+ const reason = error instanceof Error ? error.message : String(error);
1039
+ throw new Error(`Failed to reach ${serverUrl}: ${reason}`);
1040
+ }
1041
+ if (response.status !== 401) {
1042
+ return void 0;
1043
+ }
1044
+ const { resourceMetadataUrl } = (0, import_auth.extractWWWAuthenticateParams)(response);
1045
+ return resourceMetadataUrl;
1046
+ }
1047
+ function defaultStatusWriter(message) {
1048
+ import_node_process4.default.stderr.write(message);
1049
+ }
1050
+
1051
+ // src/auth/logout.ts
1052
+ var import_node_process5 = __toESM(require("process"), 1);
1053
+ async function logout(options) {
1054
+ const config = loadConfig({
1055
+ configPath: options.configPath,
1056
+ envFilePath: options.envFilePath
1057
+ });
1058
+ const entry = resolveOAuthCapableEntry2(config, options.mcpName);
1059
+ const writeStatus = options.writeStatus ?? defaultStatusWriter2;
1060
+ const keychain = new KeychainStore(options.mcpName, entry.url);
1061
+ const removed = keychain.delete();
1062
+ if (removed) {
1063
+ writeStatus(`Removed keychain credentials for "${options.mcpName}".
1064
+ `);
1065
+ } else {
1066
+ writeStatus(
1067
+ `No keychain credentials were stored for "${options.mcpName}"; nothing to remove.
1068
+ `
1069
+ );
1070
+ }
1071
+ }
1072
+ function resolveOAuthCapableEntry2(config, mcpName2) {
1073
+ const entry = config.mcp[mcpName2];
1074
+ if (entry === void 0) {
1075
+ const available = Object.keys(config.mcp).sort().join(", ");
1076
+ throw new Error(`Unknown MCP "${mcpName2}". Configured MCPs: ${available || "(none)"}.`);
1077
+ }
1078
+ if (entry.transport !== "streamable-http" && entry.transport !== "sse") {
1079
+ throw new Error(
1080
+ `MCP "${mcpName2}" uses the "${entry.transport}" transport; OAuth is only supported for streamable-http and sse upstreams.`
1081
+ );
1082
+ }
1083
+ return entry;
1084
+ }
1085
+ function defaultStatusWriter2(message) {
1086
+ import_node_process5.default.stderr.write(message);
1087
+ }
1088
+
1089
+ // src/diagnostics/list.ts
1090
+ var import_node_process6 = __toESM(require("process"), 1);
1091
+
1092
+ // src/diagnostics/format.ts
1093
+ function renderTable(headers, rows) {
1094
+ const allRows = [headers, ...rows];
1095
+ const widths = headers.map(
1096
+ (_, colIdx) => Math.max(...allRows.map((row) => (row[colIdx] ?? "").length))
1097
+ );
1098
+ return allRows.map(
1099
+ (row) => row.map((cell, i) => {
1100
+ if (i === headers.length - 1) return cell;
1101
+ return cell.padEnd(widths[i] ?? 0);
1102
+ }).join(" ").trimEnd()
1103
+ ).join("\n");
1104
+ }
1105
+ function truncate(value, max) {
1106
+ if (value.length <= max) return value;
1107
+ if (max <= 3) return value.slice(0, max);
1108
+ return `${value.slice(0, max - 3)}...`;
1109
+ }
1110
+ function humanizeDuration(seconds) {
1111
+ if (seconds < 0) return "expired";
1112
+ if (seconds < 60) return `${Math.floor(seconds)}s`;
1113
+ const totalMinutes = Math.floor(seconds / 60);
1114
+ if (totalMinutes < 60) return `${totalMinutes}m`;
1115
+ const totalHours = Math.floor(totalMinutes / 60);
1116
+ const remainingMinutes = totalMinutes % 60;
1117
+ if (totalHours < 24) {
1118
+ return remainingMinutes > 0 ? `${totalHours}h ${remainingMinutes}m` : `${totalHours}h`;
1119
+ }
1120
+ const totalDays = Math.floor(totalHours / 24);
1121
+ const remainingHours = totalHours % 24;
1122
+ return remainingHours > 0 ? `${totalDays}d ${remainingHours}h` : `${totalDays}d`;
1123
+ }
1124
+
1125
+ // src/diagnostics/list.ts
1126
+ var ENDPOINT_MAX_WIDTH = 48;
1127
+ async function list(options = {}) {
1128
+ const config = loadConfig({
1129
+ configPath: options.configPath,
1130
+ envFilePath: options.envFilePath
1131
+ });
1132
+ const write = options.write ?? ((chunk) => void import_node_process6.default.stdout.write(chunk));
1133
+ const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
1134
+ const entries = buildEntries(config, now);
1135
+ if (options.json === true) {
1136
+ write(`${JSON.stringify(entries, null, 2)}
1137
+ `);
1138
+ return;
1139
+ }
1140
+ if (entries.length === 0) {
1141
+ write("No upstream MCPs configured.\n");
1142
+ return;
1143
+ }
1144
+ const headers = ["NAME", "TRANSPORT", "MODE", "ENDPOINT", "AUTH"];
1145
+ const rows = entries.map((entry) => [
1146
+ entry.name,
1147
+ entry.transport,
1148
+ entry.mode,
1149
+ truncate(entry.endpoint, ENDPOINT_MAX_WIDTH),
1150
+ formatAuthStatus(entry.auth)
1151
+ ]);
1152
+ write(`${renderTable(headers, rows)}
1153
+ `);
1154
+ }
1155
+ function buildEntries(config, now) {
1156
+ return Object.entries(config.mcp).map(([name, entry]) => {
1157
+ const mode = entry.description !== void 0 ? "lazy" : "eager";
1158
+ if (entry.transport === "stdio") {
1159
+ const command = entry.command;
1160
+ const args = (entry.args ?? []).join(" ");
1161
+ const endpoint = args.length > 0 ? `${command} ${args}` : command;
1162
+ const built2 = {
1163
+ name,
1164
+ transport: "stdio",
1165
+ mode,
1166
+ endpoint,
1167
+ auth: { kind: "n/a" }
1168
+ };
1169
+ if (entry.description !== void 0) built2.description = entry.description;
1170
+ return built2;
1171
+ }
1172
+ const hasAuthHeader = hasBearerAuthHeader(entry.headers);
1173
+ const keychain = new KeychainStore(name, entry.url);
1174
+ const blob = keychain.get();
1175
+ let auth2;
1176
+ if (blob !== void 0) {
1177
+ const expiresInSeconds = blob.expires_at - now();
1178
+ auth2 = {
1179
+ kind: "oauth",
1180
+ status: "logged_in",
1181
+ expiresInSeconds,
1182
+ expiresAt: blob.expires_at
1183
+ };
1184
+ if (hasAuthHeader) auth2.alsoHeader = true;
1185
+ } else if (hasAuthHeader) {
1186
+ auth2 = { kind: "header" };
1187
+ } else {
1188
+ auth2 = { kind: "oauth", status: "not_logged_in" };
1189
+ }
1190
+ const built = {
1191
+ name,
1192
+ transport: entry.transport,
1193
+ mode,
1194
+ endpoint: entry.url,
1195
+ auth: auth2
1196
+ };
1197
+ if (entry.description !== void 0) built.description = entry.description;
1198
+ return built;
1199
+ });
1200
+ }
1201
+ function hasBearerAuthHeader(headers) {
1202
+ if (headers === void 0) return false;
1203
+ for (const key of Object.keys(headers)) {
1204
+ if (key.toLowerCase() === "authorization") return true;
1205
+ }
1206
+ return false;
1207
+ }
1208
+ function formatAuthStatus(auth2) {
1209
+ switch (auth2.kind) {
1210
+ case "n/a":
1211
+ return "n/a";
1212
+ case "header":
1213
+ return "header";
1214
+ case "oauth": {
1215
+ if (auth2.status === "not_logged_in") return "oauth: not logged in";
1216
+ const duration = humanizeDuration(auth2.expiresInSeconds ?? 0);
1217
+ const base = `oauth: logged in (expires in ${duration})`;
1218
+ return auth2.alsoHeader === true ? `${base} (header also set)` : base;
1219
+ }
1220
+ }
1221
+ }
1222
+
1223
+ // src/diagnostics/test.ts
1224
+ var import_node_process8 = __toESM(require("process"), 1);
1225
+
1226
+ // src/proxy/transport-factory.ts
1227
+ var import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
1228
+ var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
1229
+ var import_sse = require("@modelcontextprotocol/sdk/client/sse.js");
1230
+ function createTransport(mcpName2, config) {
1231
+ switch (config.transport) {
1232
+ case "stdio":
1233
+ return new import_stdio.StdioClientTransport({
1234
+ command: config.command,
1235
+ args: config.args,
1236
+ env: config.env
1237
+ });
1238
+ case "streamable-http":
1239
+ return new import_streamableHttp.StreamableHTTPClientTransport(new URL(config.url), {
1240
+ ...config.headers !== void 0 ? { requestInit: { headers: config.headers } } : {},
1241
+ authProvider: buildOAuthProvider(mcpName2, config)
1242
+ });
1243
+ case "sse":
1244
+ return new import_sse.SSEClientTransport(new URL(config.url), {
1245
+ ...config.headers !== void 0 ? { requestInit: { headers: config.headers } } : {},
1246
+ authProvider: buildOAuthProvider(mcpName2, config)
1247
+ });
1248
+ default: {
1249
+ const _exhaustive = config;
1250
+ return _exhaustive;
1251
+ }
1252
+ }
1253
+ }
1254
+ function buildOAuthProvider(mcpName2, config) {
1255
+ const keychain = new KeychainStore(mcpName2, config.url);
1256
+ return new ProxyOAuthProvider(mcpName2, keychain, config.auth);
1257
+ }
1258
+
1259
+ // src/proxy/upstream-client.ts
1260
+ var import_node_process7 = __toESM(require("process"), 1);
1261
+ var import_client = require("@modelcontextprotocol/sdk/client/index.js");
1262
+ var import_types4 = require("@modelcontextprotocol/sdk/types.js");
1263
+ var UpstreamClient = class {
1264
+ transport;
1265
+ onTransportError;
1266
+ notificationHandlers;
1267
+ serverRequestHandlers;
1268
+ client = null;
1269
+ constructor({
1270
+ name,
1271
+ transport,
1272
+ onTransportError,
1273
+ notifications,
1274
+ serverRequests
1275
+ }) {
1276
+ this.transport = transport;
1277
+ this.notificationHandlers = notifications ?? {};
1278
+ this.serverRequestHandlers = serverRequests ?? {};
1279
+ this.onTransportError = onTransportError ?? ((error) => {
1280
+ import_node_process7.default.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
1281
+ `);
1282
+ });
1283
+ }
1284
+ async connect() {
1285
+ this.transport.onerror = this.onTransportError;
1286
+ this.client = new import_client.Client(
1287
+ { name: "dynamic-discovery-mcp", version: "1.0.0" },
1288
+ {
1289
+ capabilities: {
1290
+ // Declare every client-side capability the proxy may relay on behalf of the host.
1291
+ // Actual reachability of each feature depends on what the host supports — if the
1292
+ // host does not support sampling, for instance, the host call returns an error
1293
+ // which we forward back to the upstream verbatim.
1294
+ sampling: {},
1295
+ elicitation: {},
1296
+ roots: { listChanged: true }
1297
+ }
1298
+ }
1299
+ );
1300
+ this.registerServerRequestHandlers(this.client);
1301
+ if (this.notificationHandlers.onToolsListChanged !== void 0) {
1302
+ this.client.setNotificationHandler(import_types4.ToolListChangedNotificationSchema, async () => {
1303
+ await this.notificationHandlers.onToolsListChanged?.();
1304
+ });
1305
+ }
1306
+ if (this.notificationHandlers.onResourcesListChanged !== void 0) {
1307
+ this.client.setNotificationHandler(import_types4.ResourceListChangedNotificationSchema, async () => {
1308
+ await this.notificationHandlers.onResourcesListChanged?.();
1309
+ });
1310
+ }
1311
+ if (this.notificationHandlers.onResourceUpdated !== void 0) {
1312
+ this.client.setNotificationHandler(import_types4.ResourceUpdatedNotificationSchema, async (notification) => {
1313
+ await this.notificationHandlers.onResourceUpdated?.({ uri: notification.params.uri });
1314
+ });
1315
+ }
1316
+ if (this.notificationHandlers.onPromptsListChanged !== void 0) {
1317
+ this.client.setNotificationHandler(import_types4.PromptListChangedNotificationSchema, async () => {
1318
+ await this.notificationHandlers.onPromptsListChanged?.();
1319
+ });
1320
+ }
1321
+ if (this.notificationHandlers.onLogMessage !== void 0) {
1322
+ this.client.setNotificationHandler(import_types4.LoggingMessageNotificationSchema, async (notification) => {
1323
+ await this.notificationHandlers.onLogMessage?.(notification.params);
1324
+ });
1325
+ }
1326
+ await this.client.connect(this.transport);
1327
+ }
1328
+ async setLoggingLevel(level, options) {
1329
+ const client = this.requireClient();
1330
+ await client.setLoggingLevel(level, options);
1331
+ }
1332
+ async listPrompts(options) {
1333
+ const client = this.requireClient();
1334
+ const result = await client.listPrompts(void 0, options);
1335
+ return result.prompts;
1336
+ }
1337
+ async getPrompt(name, args, options) {
1338
+ const client = this.requireClient();
1339
+ const params = { name };
1340
+ if (args !== void 0) {
1341
+ params.arguments = args;
1342
+ }
1343
+ return client.getPrompt(params, options);
1344
+ }
1345
+ async complete(params, options) {
1346
+ const client = this.requireClient();
1347
+ return client.complete(params, options);
1348
+ }
1349
+ /**
1350
+ * Returns the capabilities advertised by the upstream server during initialize.
1351
+ * Returns `undefined` if the client is not connected, or if the SDK has not yet
1352
+ * recorded the server's capabilities (e.g. during a partially-completed handshake).
1353
+ */
1354
+ getCapabilities() {
1355
+ return this.client?.getServerCapabilities();
1356
+ }
1357
+ async listTools(options) {
1358
+ const client = this.requireClient();
1359
+ const result = await client.listTools(void 0, options);
1360
+ return result.tools.map((tool) => {
1361
+ const upstreamTool = {
1362
+ name: tool.name,
1363
+ description: tool.description ?? "",
1364
+ inputSchema: tool.inputSchema
1365
+ };
1366
+ if (tool.outputSchema !== void 0) {
1367
+ upstreamTool.outputSchema = tool.outputSchema;
1368
+ }
1369
+ if (tool.annotations !== void 0) {
1370
+ upstreamTool.annotations = {
1371
+ title: tool.annotations.title,
1372
+ readOnlyHint: tool.annotations.readOnlyHint,
1373
+ destructiveHint: tool.annotations.destructiveHint,
1374
+ idempotentHint: tool.annotations.idempotentHint,
1375
+ openWorldHint: tool.annotations.openWorldHint
1376
+ };
1377
+ }
1378
+ return upstreamTool;
1379
+ });
1380
+ }
1381
+ async callTool(name, input, options) {
1382
+ const client = this.requireClient();
1383
+ const result = await client.callTool({ name, arguments: input }, void 0, options);
1384
+ return result;
1385
+ }
1386
+ async listResources(options) {
1387
+ const client = this.requireClient();
1388
+ const result = await client.listResources(void 0, options);
1389
+ return result.resources;
1390
+ }
1391
+ async listResourceTemplates(options) {
1392
+ const client = this.requireClient();
1393
+ const result = await client.listResourceTemplates(void 0, options);
1394
+ return result.resourceTemplates;
1395
+ }
1396
+ async readResource(uri, options) {
1397
+ const client = this.requireClient();
1398
+ return client.readResource({ uri }, options);
1399
+ }
1400
+ async subscribeResource(uri, options) {
1401
+ const client = this.requireClient();
1402
+ await client.subscribeResource({ uri }, options);
1403
+ }
1404
+ async unsubscribeResource(uri, options) {
1405
+ const client = this.requireClient();
1406
+ await client.unsubscribeResource({ uri }, options);
1407
+ }
1408
+ async disconnect() {
1409
+ if (this.client === null) {
1410
+ return;
1411
+ }
1412
+ await this.client.close();
1413
+ this.client = null;
1414
+ }
1415
+ /**
1416
+ * Sends `notifications/roots/list_changed` to the upstream, letting it know that
1417
+ * the host's set of filesystem roots has changed.
1418
+ */
1419
+ async sendRootsListChanged() {
1420
+ const client = this.requireClient();
1421
+ await client.sendRootsListChanged();
1422
+ }
1423
+ registerServerRequestHandlers(client) {
1424
+ if (this.serverRequestHandlers.onCreateMessage !== void 0) {
1425
+ client.setRequestHandler(
1426
+ import_types4.CreateMessageRequestSchema,
1427
+ async (request, extra) => {
1428
+ return this.serverRequestHandlers.onCreateMessage(request.params, {
1429
+ signal: extra.signal
1430
+ });
1431
+ }
1432
+ );
1433
+ }
1434
+ if (this.serverRequestHandlers.onElicitInput !== void 0) {
1435
+ client.setRequestHandler(
1436
+ import_types4.ElicitRequestSchema,
1437
+ async (request, extra) => {
1438
+ return this.serverRequestHandlers.onElicitInput(request.params, {
1439
+ signal: extra.signal
1440
+ });
1441
+ }
1442
+ );
1443
+ }
1444
+ if (this.serverRequestHandlers.onListRoots !== void 0) {
1445
+ client.setRequestHandler(
1446
+ import_types4.ListRootsRequestSchema,
1447
+ async (request, extra) => {
1448
+ return this.serverRequestHandlers.onListRoots(request.params, {
1449
+ signal: extra.signal
1450
+ });
1451
+ }
1452
+ );
1453
+ }
1454
+ }
1455
+ requireClient() {
1456
+ if (this.client === null) {
1457
+ throw new Error("Client is not connected. Call connect() first.");
1458
+ }
1459
+ return this.client;
1460
+ }
1461
+ };
1462
+
1463
+ // src/diagnostics/test.ts
1464
+ var DESCRIPTION_MAX_LENGTH = 100;
1465
+ var DEFAULT_TIMEOUT_MS = 15e3;
1466
+ async function test(options = {}) {
1467
+ const config = loadConfig({
1468
+ configPath: options.configPath,
1469
+ envFilePath: options.envFilePath
1470
+ });
1471
+ const write = options.write ?? ((chunk) => void import_node_process8.default.stdout.write(chunk));
1472
+ const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
1473
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1474
+ if (options.mcpName !== void 0) {
1475
+ return runSingle(config, options.mcpName, {
1476
+ write,
1477
+ now,
1478
+ timeoutMs,
1479
+ json: options.json === true
1480
+ });
1481
+ }
1482
+ return runAll(config, { write, now, timeoutMs, json: options.json === true });
1483
+ }
1484
+ async function runSingle(config, mcpName2, options) {
1485
+ const entry = config.mcp[mcpName2];
1486
+ if (entry === void 0) {
1487
+ const available = Object.keys(config.mcp).sort().join(", ");
1488
+ throw new Error(`Unknown MCP "${mcpName2}". Configured MCPs: ${available || "(none)"}.`);
1489
+ }
1490
+ if (!options.json) {
1491
+ options.write(`Testing "${mcpName2}" (${entry.transport}, ${endpointForEntry(entry)})
1492
+ `);
1493
+ }
1494
+ const result = await probeOne(mcpName2, entry, options.timeoutMs, options.now);
1495
+ if (options.json) {
1496
+ options.write(`${JSON.stringify(result, null, 2)}
1497
+ `);
1498
+ } else {
1499
+ for (const step of result.steps) {
1500
+ options.write(` [${step.status}] ${step.label}
1501
+ `);
1502
+ if (step.error !== void 0) {
1503
+ options.write(` -> ${step.error}
1504
+ `);
1505
+ }
1506
+ }
1507
+ renderDiscoveredSurface(options.write, result);
1508
+ options.write(`Result: ${result.result}
1509
+ `);
1510
+ }
1511
+ return result.result === "PASS" ? 0 : 1;
1512
+ }
1513
+ async function runAll(config, options) {
1514
+ const names = Object.keys(config.mcp);
1515
+ const total = names.length;
1516
+ if (!options.json) {
1517
+ options.write(`Testing all configured upstreams (${total})...
1518
+
1519
+ `);
1520
+ }
1521
+ const results = [];
1522
+ for (let index = 0; index < names.length; index += 1) {
1523
+ const name = names[index];
1524
+ const entry = config.mcp[name];
1525
+ if (!options.json) {
1526
+ options.write(`[${index + 1}/${total}] ${name} (${entry.transport}) ... `);
1527
+ }
1528
+ const result = await probeOne(name, entry, options.timeoutMs, options.now);
1529
+ results.push(result);
1530
+ if (!options.json) {
1531
+ if (result.result === "PASS") {
1532
+ const counts = `${result.tools?.length ?? 0} tools, ${(result.resources?.length ?? 0) + (result.resource_templates?.length ?? 0)} resources, ${result.prompts?.length ?? 0} prompts`;
1533
+ options.write(`PASS (${counts})
1534
+ `);
1535
+ } else {
1536
+ options.write(`FAIL (${result.fail_reason ?? "unknown"})
1537
+ `);
1538
+ }
1539
+ }
1540
+ }
1541
+ const passed = results.filter((r) => r.result === "PASS").length;
1542
+ const failed = results.length - passed;
1543
+ if (options.json) {
1544
+ const payload = { summary: { passed, failed }, results };
1545
+ options.write(`${JSON.stringify(payload, null, 2)}
1546
+ `);
1547
+ } else {
1548
+ options.write(`
1549
+ Summary: ${passed} passed, ${failed} failed
1550
+ `);
1551
+ }
1552
+ return failed === 0 ? 0 : 1;
1553
+ }
1554
+ async function probeOne(mcpName2, entry, timeoutMs, now) {
1555
+ const result = {
1556
+ name: mcpName2,
1557
+ result: "PASS",
1558
+ transport: entry.transport,
1559
+ endpoint: endpointForEntry(entry),
1560
+ auth: deriveAuthSummary(mcpName2, entry, now),
1561
+ steps: []
1562
+ };
1563
+ if (entry.transport !== "stdio") {
1564
+ result.steps.push({ label: authStepLabel(result.auth), status: "ok" });
1565
+ }
1566
+ const clientHolder = { value: null };
1567
+ let timeoutHandle;
1568
+ const run = (async () => {
1569
+ const transport = createTransport(mcpName2, entry);
1570
+ const client = new UpstreamClient({
1571
+ name: mcpName2,
1572
+ transport,
1573
+ onTransportError: () => {
1574
+ }
1575
+ });
1576
+ clientHolder.value = client;
1577
+ await client.connect();
1578
+ result.steps.push({ label: "Connected and initialized", status: "ok" });
1579
+ const caps = client.getCapabilities();
1580
+ result.capabilities = caps;
1581
+ result.steps.push({
1582
+ label: `Capabilities: ${describeCapabilities(caps)}`,
1583
+ status: "ok"
1584
+ });
1585
+ const tools = await client.listTools();
1586
+ result.tools = tools.map((tool) => ({
1587
+ name: tool.name,
1588
+ description: tool.description
1589
+ }));
1590
+ result.steps.push({
1591
+ label: `tools/list returned ${tools.length} tool${tools.length === 1 ? "" : "s"}`,
1592
+ status: "ok"
1593
+ });
1594
+ let resources = [];
1595
+ let templates = [];
1596
+ if (caps?.resources !== void 0) {
1597
+ try {
1598
+ resources = await client.listResources();
1599
+ templates = await client.listResourceTemplates();
1600
+ result.resources = resources.map((r) => {
1601
+ const out = {
1602
+ uri: r.uri,
1603
+ name: r.name
1604
+ };
1605
+ if (r.description !== void 0) out.description = r.description;
1606
+ return out;
1607
+ });
1608
+ result.resource_templates = templates.map((t) => {
1609
+ const out = {
1610
+ uriTemplate: t.uriTemplate,
1611
+ name: t.name
1612
+ };
1613
+ if (t.description !== void 0) out.description = t.description;
1614
+ return out;
1615
+ });
1616
+ result.steps.push({
1617
+ label: `resources/list returned ${resources.length} resource${resources.length === 1 ? "" : "s"}, ${templates.length} template${templates.length === 1 ? "" : "s"}`,
1618
+ status: "ok"
1619
+ });
1620
+ } catch (error) {
1621
+ result.steps.push({
1622
+ label: "resources/list",
1623
+ status: "fail",
1624
+ error: errorMessage(error)
1625
+ });
1626
+ }
1627
+ }
1628
+ let prompts = [];
1629
+ if (caps?.prompts !== void 0) {
1630
+ try {
1631
+ prompts = await client.listPrompts();
1632
+ result.prompts = prompts.map((p) => {
1633
+ const out = { name: p.name };
1634
+ if (p.description !== void 0) out.description = p.description;
1635
+ return out;
1636
+ });
1637
+ result.steps.push({
1638
+ label: `prompts/list returned ${prompts.length} prompt${prompts.length === 1 ? "" : "s"}`,
1639
+ status: "ok"
1640
+ });
1641
+ } catch (error) {
1642
+ result.steps.push({
1643
+ label: "prompts/list",
1644
+ status: "fail",
1645
+ error: errorMessage(error)
1646
+ });
1647
+ }
1648
+ }
1649
+ })();
1650
+ const timeout = new Promise((_, reject) => {
1651
+ timeoutHandle = setTimeout(() => {
1652
+ reject(new TestTimeoutError(timeoutMs));
1653
+ }, timeoutMs);
1654
+ });
1655
+ try {
1656
+ await Promise.race([run, timeout]);
1657
+ } catch (error) {
1658
+ result.result = "FAIL";
1659
+ result.fail_reason = failReason(error, mcpName2);
1660
+ result.steps.push({
1661
+ label: `aborted: ${result.fail_reason}`,
1662
+ status: "fail",
1663
+ error: errorMessage(error)
1664
+ });
1665
+ } finally {
1666
+ if (timeoutHandle !== void 0) clearTimeout(timeoutHandle);
1667
+ const connected = clientHolder.value;
1668
+ if (connected !== null) {
1669
+ try {
1670
+ await connected.disconnect();
1671
+ } catch {
1672
+ }
1673
+ }
1674
+ }
1675
+ if (result.result === "PASS" && result.steps.some((s) => s.status === "fail")) {
1676
+ result.result = "FAIL";
1677
+ const failed = result.steps.find((s) => s.status === "fail");
1678
+ result.fail_reason = failed?.error ?? failed?.label ?? "unknown";
1679
+ }
1680
+ return result;
1681
+ }
1682
+ var TestTimeoutError = class extends Error {
1683
+ constructor(timeoutMs) {
1684
+ super(`Test timed out after ${timeoutMs}ms`);
1685
+ this.name = "TestTimeoutError";
1686
+ }
1687
+ };
1688
+ function endpointForEntry(entry) {
1689
+ if (entry.transport === "stdio") {
1690
+ const args = (entry.args ?? []).join(" ");
1691
+ return args.length > 0 ? `${entry.command} ${args}` : entry.command;
1692
+ }
1693
+ return entry.url;
1694
+ }
1695
+ function deriveAuthSummary(mcpName2, entry, now) {
1696
+ if (entry.transport === "stdio") return { kind: "n/a" };
1697
+ const keychain = new KeychainStore(mcpName2, entry.url);
1698
+ const blob = keychain.get();
1699
+ if (blob !== void 0) {
1700
+ return {
1701
+ kind: "oauth",
1702
+ status: "valid",
1703
+ expiresInSeconds: blob.expires_at - now()
1704
+ };
1705
+ }
1706
+ const hasHeader = entry.headers !== void 0 && Object.keys(entry.headers).some((k) => k.toLowerCase() === "authorization");
1707
+ if (hasHeader) return { kind: "header" };
1708
+ return { kind: "oauth", status: "missing" };
1709
+ }
1710
+ function authStepLabel(auth2) {
1711
+ switch (auth2.kind) {
1712
+ case "n/a":
1713
+ return "(no auth applicable)";
1714
+ case "header":
1715
+ return "Static Authorization header present";
1716
+ case "oauth":
1717
+ if (auth2.status === "missing") return "No cached OAuth token";
1718
+ return `OAuth token present (expires in ${humanizeDuration(auth2.expiresInSeconds ?? 0)})`;
1719
+ }
1720
+ }
1721
+ function describeCapabilities(caps) {
1722
+ if (caps === void 0) return "(none advertised)";
1723
+ const parts = [];
1724
+ for (const [name, value] of Object.entries(caps)) {
1725
+ if (value === void 0 || value === null) continue;
1726
+ if (typeof value === "object" && Object.keys(value).length > 0) {
1727
+ const flags = Object.entries(value).filter(([, v]) => v === true).map(([k]) => k).join(",");
1728
+ parts.push(flags.length > 0 ? `${name}(${flags})` : name);
1729
+ } else {
1730
+ parts.push(name);
1731
+ }
1732
+ }
1733
+ return parts.length > 0 ? parts.join(", ") : "(none advertised)";
1734
+ }
1735
+ function failReason(error, mcpName2) {
1736
+ if (isAuthRequiredError(error)) {
1737
+ return `auth required: run \`dynmcp login ${mcpName2}\``;
1738
+ }
1739
+ if (error instanceof TestTimeoutError) return error.message;
1740
+ if (error instanceof Error) return error.message.split("\n")[0] ?? "unknown error";
1741
+ return String(error);
1742
+ }
1743
+ function errorMessage(error) {
1744
+ if (error instanceof Error) return error.message;
1745
+ return String(error);
1746
+ }
1747
+ function renderDiscoveredSurface(write, result) {
1748
+ if (result.result !== "PASS") return;
1749
+ const sections = [
1750
+ [
1751
+ `Tools (${result.tools?.length ?? 0})`,
1752
+ result.tools && result.tools.length > 0 ? () => {
1753
+ const sorted = [...result.tools ?? []].sort((a, b) => a.name.localeCompare(b.name));
1754
+ for (const tool of sorted) {
1755
+ write(` - ${tool.name}: ${truncate(tool.description, DESCRIPTION_MAX_LENGTH)}
1756
+ `);
1757
+ }
1758
+ } : void 0
1759
+ ],
1760
+ [
1761
+ `Resources (${result.resources?.length ?? 0})`,
1762
+ result.resources && result.resources.length > 0 ? () => {
1763
+ const sorted = [...result.resources ?? []].sort((a, b) => a.uri.localeCompare(b.uri));
1764
+ for (const r of sorted) {
1765
+ const tail = r.description ?? r.name;
1766
+ write(` - ${r.uri}: ${truncate(tail, DESCRIPTION_MAX_LENGTH)}
1767
+ `);
1768
+ }
1769
+ } : void 0
1770
+ ],
1771
+ [
1772
+ `Resource templates (${result.resource_templates?.length ?? 0})`,
1773
+ result.resource_templates && result.resource_templates.length > 0 ? () => {
1774
+ const sorted = [...result.resource_templates ?? []].sort(
1775
+ (a, b) => a.uriTemplate.localeCompare(b.uriTemplate)
1776
+ );
1777
+ for (const t of sorted) {
1778
+ const tail = t.description ?? t.name;
1779
+ write(` - ${t.uriTemplate}: ${truncate(tail, DESCRIPTION_MAX_LENGTH)}
1780
+ `);
1781
+ }
1782
+ } : void 0
1783
+ ],
1784
+ [
1785
+ `Prompts (${result.prompts?.length ?? 0})`,
1786
+ result.prompts && result.prompts.length > 0 ? () => {
1787
+ const sorted = [...result.prompts ?? []].sort((a, b) => a.name.localeCompare(b.name));
1788
+ for (const p of sorted) {
1789
+ const tail = p.description ?? "";
1790
+ write(
1791
+ ` - ${p.name}${tail.length > 0 ? `: ${truncate(tail, DESCRIPTION_MAX_LENGTH)}` : ""}
1792
+ `
1793
+ );
1794
+ }
1795
+ } : void 0
1796
+ ]
1797
+ ];
1798
+ for (const [header, render] of sections) {
1799
+ if (render === void 0) continue;
1800
+ write(`
1801
+ ${header}:
1802
+ `);
1803
+ render();
1804
+ }
390
1805
  }
391
1806
 
392
- // src/config/json-schema.ts
393
- var import_zod2 = require("zod");
1807
+ // src/proxy/index.ts
1808
+ var import_node_process11 = __toESM(require("process"), 1);
1809
+ var import_stdio3 = require("@modelcontextprotocol/sdk/client/stdio.js");
394
1810
 
395
1811
  // src/proxy/orchestrator.ts
396
- var import_node_process4 = __toESM(require("process"), 1);
1812
+ var import_node_process9 = __toESM(require("process"), 1);
397
1813
 
398
1814
  // src/proxy/capability-aggregator.ts
399
1815
  function aggregateCapabilities(upstreams) {
@@ -838,7 +2254,7 @@ function buildToolsBlock(groups) {
838
2254
  const sections = sortedMcpNames.map((mcpName2) => {
839
2255
  const tools = groups.get(mcpName2);
840
2256
  const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
841
- const toolLines = sortedTools.map((tool) => `- ${mcpName2}/${tool.name}: ${tool.description}`).join("\n");
2257
+ const toolLines = sortedTools.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
842
2258
  return `${mcpName2}:
843
2259
  ${toolLines}`;
844
2260
  });
@@ -893,210 +2309,6 @@ function buildAnnotationLines(tool) {
893
2309
  return lines;
894
2310
  }
895
2311
 
896
- // src/proxy/upstream-client.ts
897
- var import_node_process3 = __toESM(require("process"), 1);
898
- var import_client = require("@modelcontextprotocol/sdk/client/index.js");
899
- var import_types = require("@modelcontextprotocol/sdk/types.js");
900
- var UpstreamClient = class {
901
- transport;
902
- onTransportError;
903
- notificationHandlers;
904
- serverRequestHandlers;
905
- client = null;
906
- constructor({
907
- name,
908
- transport,
909
- onTransportError,
910
- notifications,
911
- serverRequests
912
- }) {
913
- this.transport = transport;
914
- this.notificationHandlers = notifications ?? {};
915
- this.serverRequestHandlers = serverRequests ?? {};
916
- this.onTransportError = onTransportError ?? ((error) => {
917
- import_node_process3.default.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}
918
- `);
919
- });
920
- }
921
- async connect() {
922
- this.transport.onerror = this.onTransportError;
923
- this.client = new import_client.Client(
924
- { name: "dynamic-discovery-mcp", version: "1.0.0" },
925
- {
926
- capabilities: {
927
- // Declare every client-side capability the proxy may relay on behalf of the host.
928
- // Actual reachability of each feature depends on what the host supports — if the
929
- // host does not support sampling, for instance, the host call returns an error
930
- // which we forward back to the upstream verbatim.
931
- sampling: {},
932
- elicitation: {},
933
- roots: { listChanged: true }
934
- }
935
- }
936
- );
937
- this.registerServerRequestHandlers(this.client);
938
- if (this.notificationHandlers.onToolsListChanged !== void 0) {
939
- this.client.setNotificationHandler(import_types.ToolListChangedNotificationSchema, async () => {
940
- await this.notificationHandlers.onToolsListChanged?.();
941
- });
942
- }
943
- if (this.notificationHandlers.onResourcesListChanged !== void 0) {
944
- this.client.setNotificationHandler(import_types.ResourceListChangedNotificationSchema, async () => {
945
- await this.notificationHandlers.onResourcesListChanged?.();
946
- });
947
- }
948
- if (this.notificationHandlers.onResourceUpdated !== void 0) {
949
- this.client.setNotificationHandler(import_types.ResourceUpdatedNotificationSchema, async (notification) => {
950
- await this.notificationHandlers.onResourceUpdated?.({ uri: notification.params.uri });
951
- });
952
- }
953
- if (this.notificationHandlers.onPromptsListChanged !== void 0) {
954
- this.client.setNotificationHandler(import_types.PromptListChangedNotificationSchema, async () => {
955
- await this.notificationHandlers.onPromptsListChanged?.();
956
- });
957
- }
958
- if (this.notificationHandlers.onLogMessage !== void 0) {
959
- this.client.setNotificationHandler(import_types.LoggingMessageNotificationSchema, async (notification) => {
960
- await this.notificationHandlers.onLogMessage?.(notification.params);
961
- });
962
- }
963
- await this.client.connect(this.transport);
964
- }
965
- async setLoggingLevel(level, options) {
966
- const client = this.requireClient();
967
- await client.setLoggingLevel(level, options);
968
- }
969
- async listPrompts(options) {
970
- const client = this.requireClient();
971
- const result = await client.listPrompts(void 0, options);
972
- return result.prompts;
973
- }
974
- async getPrompt(name, args, options) {
975
- const client = this.requireClient();
976
- const params = { name };
977
- if (args !== void 0) {
978
- params.arguments = args;
979
- }
980
- return client.getPrompt(params, options);
981
- }
982
- async complete(params, options) {
983
- const client = this.requireClient();
984
- return client.complete(params, options);
985
- }
986
- /**
987
- * Returns the capabilities advertised by the upstream server during initialize.
988
- * Returns `undefined` if the client is not connected, or if the SDK has not yet
989
- * recorded the server's capabilities (e.g. during a partially-completed handshake).
990
- */
991
- getCapabilities() {
992
- return this.client?.getServerCapabilities();
993
- }
994
- async listTools(options) {
995
- const client = this.requireClient();
996
- const result = await client.listTools(void 0, options);
997
- return result.tools.map((tool) => {
998
- const upstreamTool = {
999
- name: tool.name,
1000
- description: tool.description ?? "",
1001
- inputSchema: tool.inputSchema
1002
- };
1003
- if (tool.outputSchema !== void 0) {
1004
- upstreamTool.outputSchema = tool.outputSchema;
1005
- }
1006
- if (tool.annotations !== void 0) {
1007
- upstreamTool.annotations = {
1008
- title: tool.annotations.title,
1009
- readOnlyHint: tool.annotations.readOnlyHint,
1010
- destructiveHint: tool.annotations.destructiveHint,
1011
- idempotentHint: tool.annotations.idempotentHint,
1012
- openWorldHint: tool.annotations.openWorldHint
1013
- };
1014
- }
1015
- return upstreamTool;
1016
- });
1017
- }
1018
- async callTool(name, input, options) {
1019
- const client = this.requireClient();
1020
- const result = await client.callTool({ name, arguments: input }, void 0, options);
1021
- return result;
1022
- }
1023
- async listResources(options) {
1024
- const client = this.requireClient();
1025
- const result = await client.listResources(void 0, options);
1026
- return result.resources;
1027
- }
1028
- async listResourceTemplates(options) {
1029
- const client = this.requireClient();
1030
- const result = await client.listResourceTemplates(void 0, options);
1031
- return result.resourceTemplates;
1032
- }
1033
- async readResource(uri, options) {
1034
- const client = this.requireClient();
1035
- return client.readResource({ uri }, options);
1036
- }
1037
- async subscribeResource(uri, options) {
1038
- const client = this.requireClient();
1039
- await client.subscribeResource({ uri }, options);
1040
- }
1041
- async unsubscribeResource(uri, options) {
1042
- const client = this.requireClient();
1043
- await client.unsubscribeResource({ uri }, options);
1044
- }
1045
- async disconnect() {
1046
- if (this.client === null) {
1047
- return;
1048
- }
1049
- await this.client.close();
1050
- this.client = null;
1051
- }
1052
- /**
1053
- * Sends `notifications/roots/list_changed` to the upstream, letting it know that
1054
- * the host's set of filesystem roots has changed.
1055
- */
1056
- async sendRootsListChanged() {
1057
- const client = this.requireClient();
1058
- await client.sendRootsListChanged();
1059
- }
1060
- registerServerRequestHandlers(client) {
1061
- if (this.serverRequestHandlers.onCreateMessage !== void 0) {
1062
- client.setRequestHandler(
1063
- import_types.CreateMessageRequestSchema,
1064
- async (request, extra) => {
1065
- return this.serverRequestHandlers.onCreateMessage(request.params, {
1066
- signal: extra.signal
1067
- });
1068
- }
1069
- );
1070
- }
1071
- if (this.serverRequestHandlers.onElicitInput !== void 0) {
1072
- client.setRequestHandler(
1073
- import_types.ElicitRequestSchema,
1074
- async (request, extra) => {
1075
- return this.serverRequestHandlers.onElicitInput(request.params, {
1076
- signal: extra.signal
1077
- });
1078
- }
1079
- );
1080
- }
1081
- if (this.serverRequestHandlers.onListRoots !== void 0) {
1082
- client.setRequestHandler(
1083
- import_types.ListRootsRequestSchema,
1084
- async (request, extra) => {
1085
- return this.serverRequestHandlers.onListRoots(request.params, {
1086
- signal: extra.signal
1087
- });
1088
- }
1089
- );
1090
- }
1091
- }
1092
- requireClient() {
1093
- if (this.client === null) {
1094
- throw new Error("Client is not connected. Call connect() first.");
1095
- }
1096
- return this.client;
1097
- }
1098
- };
1099
-
1100
2312
  // src/proxy/upstream-registry.ts
1101
2313
  var UpstreamRegistry = class {
1102
2314
  clients = /* @__PURE__ */ new Map();
@@ -1348,6 +2560,9 @@ var Orchestrator = class {
1348
2560
  }
1349
2561
  } catch (error) {
1350
2562
  await this.registry.deleteOne(mcpName2);
2563
+ if (isAuthRequiredError(error)) {
2564
+ throw error;
2565
+ }
1351
2566
  const failures = this.lazyRegistry.recordFailure(mcpName2);
1352
2567
  if (failures >= MAX_LOAD_ATTEMPTS) {
1353
2568
  this.lazyRegistry.take(mcpName2);
@@ -1594,7 +2809,7 @@ var Orchestrator = class {
1594
2809
  targets.push(
1595
2810
  action(client).catch((error) => {
1596
2811
  const message = error instanceof Error ? error.message : String(error);
1597
- import_node_process4.default.stderr.write(`dynmcp: ${label} failed for "${mcpName2}": ${message}
2812
+ import_node_process9.default.stderr.write(`dynmcp: ${label} failed for "${mcpName2}": ${message}
1598
2813
  `);
1599
2814
  })
1600
2815
  );
@@ -1619,13 +2834,13 @@ function splitNamespacedName(namespacedName, knownMcpNames) {
1619
2834
  }
1620
2835
  function logCollisions(resourceRouter, promptRouter) {
1621
2836
  for (const collision of resourceRouter.collisions()) {
1622
- import_node_process4.default.stderr.write(
2837
+ import_node_process9.default.stderr.write(
1623
2838
  `dynmcp: resource URI collision: "${collision.uri}" is provided by "${collision.chosen}" and "${collision.shadowed}"; routing to "${collision.chosen}".
1624
2839
  `
1625
2840
  );
1626
2841
  }
1627
2842
  for (const collision of promptRouter.collisions()) {
1628
- import_node_process4.default.stderr.write(
2843
+ import_node_process9.default.stderr.write(
1629
2844
  `dynmcp: prompt name collision: "${collision.name}" is provided by "${collision.chosen}" and "${collision.shadowed}"; routing to "${collision.chosen}".
1630
2845
  `
1631
2846
  );
@@ -1633,10 +2848,10 @@ function logCollisions(resourceRouter, promptRouter) {
1633
2848
  }
1634
2849
 
1635
2850
  // src/proxy/server.ts
1636
- var import_node_process5 = __toESM(require("process"), 1);
2851
+ var import_node_process10 = __toESM(require("process"), 1);
1637
2852
  var import_server = require("@modelcontextprotocol/sdk/server/index.js");
1638
- var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
1639
- var import_types2 = require("@modelcontextprotocol/sdk/types.js");
2853
+ var import_stdio2 = require("@modelcontextprotocol/sdk/server/stdio.js");
2854
+ var import_types5 = require("@modelcontextprotocol/sdk/types.js");
1640
2855
  var import_zod3 = require("zod");
1641
2856
  var DISCOVER_TOOL_NAME = "discover_tool";
1642
2857
  var USE_TOOL_NAME = "use_tool";
@@ -1728,7 +2943,7 @@ var ProxyServer = class {
1728
2943
  }
1729
2944
  if (this.onRootsListChangedCallback !== void 0) {
1730
2945
  const callback = this.onRootsListChangedCallback;
1731
- server.setNotificationHandler(import_types2.RootsListChangedNotificationSchema, async () => {
2946
+ server.setNotificationHandler(import_types5.RootsListChangedNotificationSchema, async () => {
1732
2947
  await callback();
1733
2948
  });
1734
2949
  }
@@ -1766,8 +2981,8 @@ var ProxyServer = class {
1766
2981
  }
1767
2982
  async start() {
1768
2983
  const server = this.buildServer();
1769
- const transport = new import_stdio.StdioServerTransport();
1770
- import_node_process5.default.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
2984
+ const transport = new import_stdio2.StdioServerTransport();
2985
+ import_node_process10.default.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
1771
2986
  await server.connect(transport);
1772
2987
  }
1773
2988
  /**
@@ -1834,7 +3049,7 @@ var ProxyServer = class {
1834
3049
  return options;
1835
3050
  }
1836
3051
  registerToolHandlers(server) {
1837
- server.setRequestHandler(import_types2.ListToolsRequestSchema, async () => {
3052
+ server.setRequestHandler(import_types5.ListToolsRequestSchema, async () => {
1838
3053
  const tools = [
1839
3054
  {
1840
3055
  name: DISCOVER_TOOL_NAME,
@@ -1857,7 +3072,7 @@ var ProxyServer = class {
1857
3072
  return { tools };
1858
3073
  });
1859
3074
  server.setRequestHandler(
1860
- import_types2.CallToolRequestSchema,
3075
+ import_types5.CallToolRequestSchema,
1861
3076
  async (request, extra) => {
1862
3077
  const { name, arguments: rawArgs } = request.params;
1863
3078
  const catalog = this.catalog();
@@ -1908,28 +3123,28 @@ var ProxyServer = class {
1908
3123
  }
1909
3124
  registerResourceHandlers(server, callbacks) {
1910
3125
  server.setRequestHandler(
1911
- import_types2.ListResourcesRequestSchema,
3126
+ import_types5.ListResourcesRequestSchema,
1912
3127
  async () => ({
1913
3128
  resources: callbacks.listResources()
1914
3129
  })
1915
3130
  );
1916
3131
  server.setRequestHandler(
1917
- import_types2.ListResourceTemplatesRequestSchema,
3132
+ import_types5.ListResourceTemplatesRequestSchema,
1918
3133
  async () => ({
1919
3134
  resourceTemplates: callbacks.listResourceTemplates()
1920
3135
  })
1921
3136
  );
1922
3137
  server.setRequestHandler(
1923
- import_types2.ReadResourceRequestSchema,
3138
+ import_types5.ReadResourceRequestSchema,
1924
3139
  async (request, extra) => {
1925
3140
  return callbacks.readResource(request.params.uri, this.buildCallOptions(request, extra));
1926
3141
  }
1927
3142
  );
1928
- server.setRequestHandler(import_types2.SubscribeRequestSchema, async (request, extra) => {
3143
+ server.setRequestHandler(import_types5.SubscribeRequestSchema, async (request, extra) => {
1929
3144
  await callbacks.subscribeResource(request.params.uri, this.buildCallOptions(request, extra));
1930
3145
  return {};
1931
3146
  });
1932
- server.setRequestHandler(import_types2.UnsubscribeRequestSchema, async (request, extra) => {
3147
+ server.setRequestHandler(import_types5.UnsubscribeRequestSchema, async (request, extra) => {
1933
3148
  await callbacks.unsubscribeResource(
1934
3149
  request.params.uri,
1935
3150
  this.buildCallOptions(request, extra)
@@ -1939,13 +3154,13 @@ var ProxyServer = class {
1939
3154
  }
1940
3155
  registerPromptHandlers(server, callbacks) {
1941
3156
  server.setRequestHandler(
1942
- import_types2.ListPromptsRequestSchema,
3157
+ import_types5.ListPromptsRequestSchema,
1943
3158
  async () => ({
1944
3159
  prompts: callbacks.listPrompts()
1945
3160
  })
1946
3161
  );
1947
3162
  server.setRequestHandler(
1948
- import_types2.GetPromptRequestSchema,
3163
+ import_types5.GetPromptRequestSchema,
1949
3164
  async (request, extra) => {
1950
3165
  return callbacks.getPrompt(
1951
3166
  request.params.name,
@@ -1957,14 +3172,14 @@ var ProxyServer = class {
1957
3172
  }
1958
3173
  registerCompletionHandler(server, callback) {
1959
3174
  server.setRequestHandler(
1960
- import_types2.CompleteRequestSchema,
3175
+ import_types5.CompleteRequestSchema,
1961
3176
  async (request, extra) => {
1962
3177
  return callback(request.params, this.buildCallOptions(request, extra));
1963
3178
  }
1964
3179
  );
1965
3180
  }
1966
3181
  registerLoggingHandler(server, callback) {
1967
- server.setRequestHandler(import_types2.SetLevelRequestSchema, async (request, extra) => {
3182
+ server.setRequestHandler(import_types5.SetLevelRequestSchema, async (request, extra) => {
1968
3183
  await callback(request.params.level, this.buildCallOptions(request, extra));
1969
3184
  return {};
1970
3185
  });
@@ -1980,35 +3195,6 @@ var ProxyServer = class {
1980
3195
  }
1981
3196
  };
1982
3197
 
1983
- // src/proxy/transport-factory.ts
1984
- var import_stdio2 = require("@modelcontextprotocol/sdk/client/stdio.js");
1985
- var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
1986
- var import_sse = require("@modelcontextprotocol/sdk/client/sse.js");
1987
- function createTransport(config) {
1988
- switch (config.transport) {
1989
- case "stdio":
1990
- return new import_stdio2.StdioClientTransport({
1991
- command: config.command,
1992
- args: config.args,
1993
- env: config.env
1994
- });
1995
- case "streamable-http":
1996
- return new import_streamableHttp.StreamableHTTPClientTransport(
1997
- new URL(config.url),
1998
- config.headers ? { requestInit: { headers: config.headers } } : void 0
1999
- );
2000
- case "sse":
2001
- return new import_sse.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
- import_node_process6.default.stderr.write(
3237
+ import_node_process11.default.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
- import_node_process6.default.stderr.write(
3251
+ import_node_process11.default.stderr.write(
2066
3252
  `dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
2067
3253
  `
2068
3254
  );
2069
- }).finally(() => import_node_process6.default.exit(exitCode));
3255
+ }).finally(() => import_node_process11.default.exit(exitCode));
2070
3256
  };
2071
3257
  activeShutdown.shutdown = shutdown;
2072
3258
  try {
2073
3259
  await orchestrator.connect();
2074
3260
  } catch (error) {
2075
- import_node_process6.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
3261
+ import_node_process11.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
2076
3262
  `);
2077
- import_node_process6.default.exit(1);
3263
+ import_node_process11.default.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
- import_node_process6.default.on("SIGINT", () => shutdown(0));
2115
- import_node_process6.default.on("SIGTERM", () => shutdown(0));
2116
- import_node_process6.default.stdin.on("end", () => shutdown(0));
2117
- import_node_process6.default.stdin.on("close", () => shutdown(0));
3300
+ import_node_process11.default.on("SIGINT", () => shutdown(0));
3301
+ import_node_process11.default.on("SIGTERM", () => shutdown(0));
3302
+ import_node_process11.default.stdin.on("end", () => shutdown(0));
3303
+ import_node_process11.default.stdin.on("close", () => shutdown(0));
2118
3304
  try {
2119
3305
  await proxyServer.start();
2120
3306
  } catch (error) {
@@ -2133,41 +3319,98 @@ var cliBanner = import_chalk.default.bold.magentaBright(
2133
3319
  );
2134
3320
  var cli = new import_commander.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 = import_node_process7.default.argv.indexOf("--");
3324
+ const separatorIndex = import_node_process12.default.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] = import_node_process7.default.argv.slice(separatorIndex + 1);
3328
+ const [command, ...args] = import_node_process12.default.argv.slice(separatorIndex + 1);
2143
3329
  if (command === void 0) {
2144
- import_node_process7.default.stderr.write(
3330
+ import_node_process12.default.stderr.write(
2145
3331
  "dynmcp: no upstream command provided after --.\nUsage: dynmcp -- <command> [args...]\n"
2146
3332
  );
2147
- import_node_process7.default.exit(1);
3333
+ import_node_process12.default.exit(1);
2148
3334
  }
2149
3335
  try {
2150
3336
  await startProxy(command, args);
2151
3337
  } catch (error) {
2152
- import_node_process7.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
3338
+ import_node_process12.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
2153
3339
  `);
2154
- import_node_process7.default.exit(1);
3340
+ import_node_process12.default.exit(1);
2155
3341
  }
2156
3342
  return;
2157
3343
  }
2158
3344
  try {
2159
3345
  await startProxyFromConfig({ configPath, envFilePath });
2160
3346
  } catch (error) {
2161
- import_node_process7.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
3347
+ import_node_process12.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
3348
+ `);
3349
+ import_node_process12.default.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
+ import_node_process12.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
3361
+ `);
3362
+ import_node_process12.default.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
+ import_node_process12.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
3374
+ `);
3375
+ import_node_process12.default.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
+ import_node_process12.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
2162
3387
  `);
2163
- import_node_process7.default.exit(1);
3388
+ import_node_process12.default.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) import_node_process12.default.exit(exitCode);
3402
+ } catch (error) {
3403
+ import_node_process12.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
3404
+ `);
3405
+ import_node_process12.default.exit(1);
3406
+ }
3407
+ }
3408
+ );
2166
3409
 
2167
3410
  // src/index.ts
2168
- var import_node_process8 = __toESM(require("process"), 1);
3411
+ var import_node_process13 = __toESM(require("process"), 1);
2169
3412
  async function main() {
2170
- cli.parse(import_node_process8.default.argv);
3413
+ cli.parse(import_node_process13.default.argv);
2171
3414
  }
2172
3415
  main();
2173
3416
  //# sourceMappingURL=index.cjs.map