@kwonye/mcpx 0.1.0 → 0.1.42

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.
Files changed (79) hide show
  1. package/dist/adapters/claude-desktop.d.ts +8 -0
  2. package/dist/adapters/claude-desktop.js +116 -0
  3. package/dist/adapters/claude-desktop.js.map +1 -0
  4. package/dist/adapters/claude.d.ts +2 -1
  5. package/dist/adapters/claude.js +70 -23
  6. package/dist/adapters/claude.js.map +1 -1
  7. package/dist/adapters/cline.d.ts +2 -1
  8. package/dist/adapters/cline.js +72 -1
  9. package/dist/adapters/cline.js.map +1 -1
  10. package/dist/adapters/codex.d.ts +2 -1
  11. package/dist/adapters/codex.js +77 -2
  12. package/dist/adapters/codex.js.map +1 -1
  13. package/dist/adapters/cursor.d.ts +2 -1
  14. package/dist/adapters/cursor.js +72 -1
  15. package/dist/adapters/cursor.js.map +1 -1
  16. package/dist/adapters/index.js +9 -1
  17. package/dist/adapters/index.js.map +1 -1
  18. package/dist/adapters/kiro.d.ts +8 -0
  19. package/dist/adapters/kiro.js +122 -0
  20. package/dist/adapters/kiro.js.map +1 -0
  21. package/dist/adapters/opencode.d.ts +8 -0
  22. package/dist/adapters/opencode.js +124 -0
  23. package/dist/adapters/opencode.js.map +1 -0
  24. package/dist/adapters/qwen.d.ts +8 -0
  25. package/dist/adapters/qwen.js +114 -0
  26. package/dist/adapters/qwen.js.map +1 -0
  27. package/dist/adapters/{utils.d.ts → utils/index.d.ts} +9 -1
  28. package/dist/adapters/{utils.js → utils/index.js} +59 -2
  29. package/dist/adapters/utils/index.js.map +1 -0
  30. package/dist/adapters/vscode.d.ts +2 -1
  31. package/dist/adapters/vscode.js +72 -1
  32. package/dist/adapters/vscode.js.map +1 -1
  33. package/dist/cli.js +586 -23
  34. package/dist/cli.js.map +1 -1
  35. package/dist/compat/claude.d.ts +28 -0
  36. package/dist/compat/claude.js +252 -0
  37. package/dist/compat/claude.js.map +1 -0
  38. package/dist/compat/codex.d.ts +22 -0
  39. package/dist/compat/codex.js +179 -0
  40. package/dist/compat/codex.js.map +1 -0
  41. package/dist/compat/index.d.ts +28 -0
  42. package/dist/compat/index.js +108 -0
  43. package/dist/compat/index.js.map +1 -0
  44. package/dist/compat/qwen.d.ts +29 -0
  45. package/dist/compat/qwen.js +316 -0
  46. package/dist/compat/qwen.js.map +1 -0
  47. package/dist/compat/unsupported.d.ts +24 -0
  48. package/dist/compat/unsupported.js +47 -0
  49. package/dist/compat/unsupported.js.map +1 -0
  50. package/dist/compat/vscode.d.ts +21 -0
  51. package/dist/compat/vscode.js +164 -0
  52. package/dist/compat/vscode.js.map +1 -0
  53. package/dist/core/auth-probe.d.ts +9 -0
  54. package/dist/core/auth-probe.js +54 -0
  55. package/dist/core/auth-probe.js.map +1 -0
  56. package/dist/core/config.js +1 -1
  57. package/dist/core/config.js.map +1 -1
  58. package/dist/core/index.d.ts +15 -0
  59. package/dist/core/index.js +21 -0
  60. package/dist/core/index.js.map +1 -0
  61. package/dist/core/registry.d.ts +1 -0
  62. package/dist/core/registry.js +7 -0
  63. package/dist/core/registry.js.map +1 -1
  64. package/dist/core/status.d.ts +32 -0
  65. package/dist/core/status.js +63 -0
  66. package/dist/core/status.js.map +1 -0
  67. package/dist/core/sync.d.ts +2 -1
  68. package/dist/core/sync.js +70 -7
  69. package/dist/core/sync.js.map +1 -1
  70. package/dist/gateway/server.js +33 -3
  71. package/dist/gateway/server.js.map +1 -1
  72. package/dist/types.d.ts +54 -1
  73. package/dist/version.d.ts +1 -1
  74. package/dist/version.js +1 -1
  75. package/dist/version.js.map +1 -1
  76. package/package.json +14 -3
  77. package/LICENSE +0 -21
  78. package/README.md +0 -150
  79. package/dist/adapters/utils.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,17 +1,26 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * mcpx CLI - HTTP-first Model Context Protocol gateway
4
+ */
2
5
  import { Command } from "commander";
3
6
  import fs from "node:fs";
4
7
  import { execFileSync } from "node:child_process";
8
+ import { emitKeypressEvents } from "node:readline";
9
+ import { createInterface } from "node:readline/promises";
5
10
  import { loadConfig, saveConfig } from "./core/config.js";
6
11
  import { addServer, removeServer } from "./core/registry.js";
7
12
  import { SecretsManager, readSecretValueFromStdin } from "./core/secrets.js";
13
+ import { probeHttpAuthRequirement } from "./core/auth-probe.js";
8
14
  import { syncAllClients } from "./core/sync.js";
15
+ import { parseCompatibilityArgs } from "./compat/index.js";
9
16
  import { applyAuthReference, defaultAuthSecretName, listAuthBindings, maybePrefixBearer, removeAuthReference, resolveAuthTarget, secretRefName, toSecretRef } from "./core/server-auth.js";
10
17
  import { getAdapters } from "./adapters/index.js";
11
18
  import { getDaemonStatus, readDaemonLogs, restartDaemon, runDaemonForeground, startDaemon, stopDaemon } from "./core/daemon.js";
12
19
  import { getConfigPath, getManagedIndexPath } from "./core/paths.js";
20
+ import { loadManagedIndex } from "./core/managed-index.js";
21
+ import { STATUS_CLIENTS, buildStatusReport } from "./core/status.js";
13
22
  import { APP_VERSION } from "./version.js";
14
- const VALID_CLIENTS = ["claude", "codex", "cursor", "cline", "vscode"];
23
+ const VALID_CLIENTS = STATUS_CLIENTS;
15
24
  function parseKeyValueFlag(value, label) {
16
25
  const split = value.indexOf("=");
17
26
  if (split <= 0 || split >= value.length - 1) {
@@ -127,6 +136,32 @@ function printSyncSummary(summary, asJson = false) {
127
136
  process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
128
137
  return;
129
138
  }
139
+ if (summary.imports.imported.length > 0
140
+ || summary.imports.duplicates.length > 0
141
+ || summary.imports.skipped.length > 0
142
+ || summary.imports.conflicts.length > 0
143
+ || summary.imports.errors.length > 0) {
144
+ process.stdout.write("Imports:\n");
145
+ for (const entry of summary.imports.imported) {
146
+ process.stdout.write(`- imported ${entry.serverName} from ${entry.clientId}`);
147
+ if (entry.sourceEntryName !== entry.serverName) {
148
+ process.stdout.write(` (${entry.sourceEntryName})`);
149
+ }
150
+ process.stdout.write("\n");
151
+ }
152
+ for (const entry of summary.imports.duplicates) {
153
+ process.stdout.write(`- adopted existing ${entry.serverName} from ${entry.clientId}\n`);
154
+ }
155
+ for (const entry of summary.imports.skipped) {
156
+ process.stdout.write(`- skipped ${entry.serverName} from ${entry.clientId} - ${entry.reason}\n`);
157
+ }
158
+ for (const entry of summary.imports.conflicts) {
159
+ process.stdout.write(`- conflict ${entry.serverName} from ${entry.clientId} - ${entry.message}\n`);
160
+ }
161
+ for (const entry of summary.imports.errors) {
162
+ process.stdout.write(`- import error ${entry.clientId} - ${entry.message}\n`);
163
+ }
164
+ }
130
165
  process.stdout.write(`Gateway: ${summary.gatewayUrl}\n`);
131
166
  for (const result of summary.results) {
132
167
  process.stdout.write(`- ${result.clientId}: ${result.status}`);
@@ -156,6 +191,528 @@ async function autoSyncManagedEntries(config) {
156
191
  printSyncSummary(summary, false);
157
192
  ensureExitCodeForSyncFailures(summary.hasErrors);
158
193
  }
194
+ function isNegativeChoice(value) {
195
+ const normalized = value.trim().toLowerCase();
196
+ return normalized === "n" || normalized === "no";
197
+ }
198
+ async function maybeAutoConfigureAuthForAddedServer(serverName, spec, secrets) {
199
+ if (spec.transport !== "http") {
200
+ return;
201
+ }
202
+ const probe = await probeHttpAuthRequirement(spec, secrets);
203
+ if (!probe.authRequired) {
204
+ return;
205
+ }
206
+ process.stdout.write(`Upstream "${serverName}" responded with ${probe.status ?? 401} and appears to require auth.\n`);
207
+ if (probe.wwwAuthenticate) {
208
+ process.stdout.write(`WWW-Authenticate: ${probe.wwwAuthenticate}\n`);
209
+ }
210
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
211
+ process.stdout.write(`Run \`mcpx auth set ${serverName} --header Authorization --value \"<token>\"\` to configure auth.\n`);
212
+ return;
213
+ }
214
+ const rl = createInterface({
215
+ input: process.stdin,
216
+ output: process.stdout
217
+ });
218
+ try {
219
+ const shouldConfigure = await promptLine(rl, "Configure auth now? (Y/n): ");
220
+ if (isNegativeChoice(shouldConfigure)) {
221
+ process.stdout.write("Skipping auth setup.\n");
222
+ return;
223
+ }
224
+ const currentHeaderBinding = listAuthBindings(spec)
225
+ .find((binding) => binding.kind === "header");
226
+ const headerName = await promptLineWithDefault(rl, "Header name", currentHeaderBinding?.key ?? "Authorization");
227
+ if (!headerName) {
228
+ process.stdout.write("Skipping auth setup (empty header name).\n");
229
+ return;
230
+ }
231
+ const authValueInput = await promptLine(rl, "Auth value/token (blank to skip): ");
232
+ if (!authValueInput) {
233
+ process.stdout.write("Skipping auth setup.\n");
234
+ return;
235
+ }
236
+ const target = resolveAuthTarget(spec, headerName, undefined);
237
+ const authValue = maybePrefixBearer(target, authValueInput, false);
238
+ const existingValue = listAuthBindings(spec)
239
+ .find((binding) => binding.kind === target.kind && binding.key === target.key)?.value;
240
+ const defaultSecret = secretRefName(existingValue ?? "") ?? defaultAuthSecretName(serverName, target);
241
+ const secretName = await promptLineWithDefault(rl, "Secret name", defaultSecret);
242
+ if (!secretName) {
243
+ process.stdout.write("Skipping auth setup (empty secret name).\n");
244
+ return;
245
+ }
246
+ secrets.setSecret(secretName, authValue);
247
+ applyAuthReference(spec, target, toSecretRef(secretName));
248
+ process.stdout.write(`Configured auth via ${target.kind}:${target.key} using secret://${secretName}.\n`);
249
+ }
250
+ catch (error) {
251
+ process.stdout.write(`Auto auth setup skipped: ${error.message}\n`);
252
+ return;
253
+ }
254
+ finally {
255
+ rl.close();
256
+ }
257
+ const verify = await probeHttpAuthRequirement(spec, secrets);
258
+ if (verify.authRequired) {
259
+ process.stdout.write("Auth is configured, but upstream still reports auth required. Re-auth may be needed.\n");
260
+ }
261
+ else if (verify.error) {
262
+ process.stdout.write(`Auth check after setup could not be completed: ${verify.error}\n`);
263
+ }
264
+ else {
265
+ process.stdout.write("Auth check passed.\n");
266
+ }
267
+ }
268
+ function loadStatusReport() {
269
+ const config = loadConfig();
270
+ return buildStatusReport(config, loadManagedIndex(), getDaemonStatus(config));
271
+ }
272
+ function daemonEmoji(running) {
273
+ return running ? "✅" : "⚠️";
274
+ }
275
+ function clientStatusEmoji(status) {
276
+ if (status === "SYNCED") {
277
+ return "✅";
278
+ }
279
+ if (status === "ERROR") {
280
+ return "❌";
281
+ }
282
+ if (status === "UNSUPPORTED_HTTP") {
283
+ return "⚠️";
284
+ }
285
+ return "⏭️";
286
+ }
287
+ function serverHealthEmoji(server) {
288
+ const managed = server.clients.filter((client) => client.managed);
289
+ if (managed.length === 0) {
290
+ return "⚠️";
291
+ }
292
+ if (managed.some((client) => client.status === "ERROR")) {
293
+ return "❌";
294
+ }
295
+ if (managed.some((client) => client.status === "UNSUPPORTED_HTTP")) {
296
+ return "⚠️";
297
+ }
298
+ return "✅";
299
+ }
300
+ function formatDaemonState(status) {
301
+ if (status.running) {
302
+ return `running (pid ${status.pid})`;
303
+ }
304
+ return "stopped";
305
+ }
306
+ function formatAuthSummary(authBindings) {
307
+ return authBindings.map((binding) => `${binding.kind}:${binding.key}`).join(", ");
308
+ }
309
+ function listSyncedClientLabels(server) {
310
+ return server.clients
311
+ .filter((client) => client.managed)
312
+ .map((client) => (`${clientStatusEmoji(client.status)} ${client.configPath ? `${client.clientId} (${client.configPath})` : client.clientId}${client.status === "SYNCED" ? "" : ` [${client.status}]`}`));
313
+ }
314
+ function printSyncedConfigBullets(server, indent) {
315
+ const synced = listSyncedClientLabels(server);
316
+ if (synced.length === 0) {
317
+ process.stdout.write(`${indent}- ⚠️ none\n`);
318
+ return;
319
+ }
320
+ for (const entry of synced) {
321
+ process.stdout.write(`${indent}- ${entry}\n`);
322
+ }
323
+ }
324
+ function printStatusReportText(report) {
325
+ process.stdout.write(`Gateway URL: ${report.gatewayUrl}\n`);
326
+ process.stdout.write(`Daemon: ${daemonEmoji(report.daemon.running)} ${formatDaemonState(report.daemon)}\n`);
327
+ process.stdout.write(`Upstream servers: ${report.upstreamCount}\n`);
328
+ if (report.servers.length === 0) {
329
+ process.stdout.write("⚠️ No upstream servers configured.\n");
330
+ }
331
+ else {
332
+ for (const server of report.servers) {
333
+ process.stdout.write(`- ${serverHealthEmoji(server)} ${server.name} (${server.transport})\n`);
334
+ process.stdout.write(` target: ${server.target}\n`);
335
+ if (server.authBindings.length > 0) {
336
+ process.stdout.write(` auth: 🔐 ${formatAuthSummary(server.authBindings)}\n`);
337
+ }
338
+ process.stdout.write(" synced configs:\n");
339
+ printSyncedConfigBullets(server, " ");
340
+ }
341
+ }
342
+ process.stdout.write("Client sync states:\n");
343
+ for (const client of STATUS_CLIENTS) {
344
+ const state = report.clients[client];
345
+ if (!state) {
346
+ process.stdout.write(`- ${clientStatusEmoji("SKIPPED")} ${client}: SKIPPED\n`);
347
+ }
348
+ else {
349
+ process.stdout.write(`- ${clientStatusEmoji(state.status)} ${client}: ${state.status}${state.message ? ` - ${state.message}` : ""}\n`);
350
+ }
351
+ }
352
+ }
353
+ async function promptLine(rl, prompt) {
354
+ return (await rl.question(prompt)).trim();
355
+ }
356
+ async function promptLineWithDefault(rl, prompt, defaultValue) {
357
+ const suffix = defaultValue ? ` [${defaultValue}]` : "";
358
+ const value = (await rl.question(`${prompt}${suffix}: `)).trim();
359
+ return value || defaultValue;
360
+ }
361
+ function parseSelection(value, max) {
362
+ const numeric = Number(value);
363
+ if (!Number.isInteger(numeric) || numeric < 1 || numeric > max) {
364
+ return null;
365
+ }
366
+ return numeric - 1;
367
+ }
368
+ function clearTerminalScreen() {
369
+ process.stdout.write("\x1b[2J\x1b[H");
370
+ }
371
+ function renderMenuScreen(titleLines, options, selectedIndex, cancelHint) {
372
+ clearTerminalScreen();
373
+ for (const line of titleLines) {
374
+ process.stdout.write(`${line}\n`);
375
+ }
376
+ process.stdout.write("\n");
377
+ options.forEach((option, index) => {
378
+ const cursor = index === selectedIndex ? "❯" : " ";
379
+ process.stdout.write(`${cursor} ${index + 1}. ${option.label}\n`);
380
+ if (option.detail) {
381
+ process.stdout.write(` ${option.detail}\n`);
382
+ }
383
+ });
384
+ process.stdout.write(`\n↑/↓ to navigate • Space/Enter to select • Esc/${cancelHint} to cancel\n`);
385
+ }
386
+ async function promptMenuSelection(rl, titleLines, options, cancelHint = "q") {
387
+ if (options.length === 0) {
388
+ return null;
389
+ }
390
+ const stdin = process.stdin;
391
+ const canUseKeyNavigation = process.stdin.isTTY && process.stdout.isTTY && typeof stdin.setRawMode === "function";
392
+ if (!canUseKeyNavigation) {
393
+ for (const line of titleLines) {
394
+ process.stdout.write(`${line}\n`);
395
+ }
396
+ process.stdout.write("\n");
397
+ options.forEach((option, index) => {
398
+ process.stdout.write(`${index + 1}. ${option.label}\n`);
399
+ if (option.detail) {
400
+ process.stdout.write(` ${option.detail}\n`);
401
+ }
402
+ });
403
+ const selected = parseSelection(await promptLine(rl, `Select option (1-${options.length}, blank to cancel): `), options.length);
404
+ return selected;
405
+ }
406
+ return new Promise((resolve) => {
407
+ let index = 0;
408
+ let settled = false;
409
+ rl.pause();
410
+ const cleanup = () => {
411
+ stdin.off("keypress", onKeypress);
412
+ try {
413
+ stdin.setRawMode(false);
414
+ }
415
+ catch {
416
+ // ignore cleanup errors
417
+ }
418
+ rl.resume();
419
+ };
420
+ const done = (value) => {
421
+ if (settled) {
422
+ return;
423
+ }
424
+ settled = true;
425
+ cleanup();
426
+ process.stdout.write("\n");
427
+ resolve(value);
428
+ };
429
+ const onKeypress = (_chunk, key) => {
430
+ if (key.ctrl && key.name === "c") {
431
+ done(null);
432
+ return;
433
+ }
434
+ if (key.name === "up" || key.name === "k") {
435
+ index = (index - 1 + options.length) % options.length;
436
+ renderMenuScreen(titleLines, options, index, cancelHint);
437
+ return;
438
+ }
439
+ if (key.name === "down" || key.name === "j") {
440
+ index = (index + 1) % options.length;
441
+ renderMenuScreen(titleLines, options, index, cancelHint);
442
+ return;
443
+ }
444
+ if (key.name === "return" || key.name === "space") {
445
+ done(index);
446
+ return;
447
+ }
448
+ if (key.name === "escape" || key.name === "q") {
449
+ done(null);
450
+ return;
451
+ }
452
+ if (key.sequence && /^[1-9]$/.test(key.sequence)) {
453
+ const numeric = Number(key.sequence);
454
+ if (numeric >= 1 && numeric <= options.length) {
455
+ done(numeric - 1);
456
+ }
457
+ }
458
+ };
459
+ emitKeypressEvents(stdin);
460
+ stdin.setRawMode(true);
461
+ stdin.resume();
462
+ stdin.on("keypress", onKeypress);
463
+ renderMenuScreen(titleLines, options, index, cancelHint);
464
+ });
465
+ }
466
+ async function chooseAuthBinding(rl, bindings, prompt) {
467
+ if (bindings.length === 0) {
468
+ return null;
469
+ }
470
+ if (bindings.length === 1) {
471
+ return bindings[0];
472
+ }
473
+ const selected = await promptMenuSelection(rl, ["Auth bindings", prompt], bindings.map((binding) => ({
474
+ label: `${binding.kind}:${binding.key}`,
475
+ detail: binding.secretName ? `source: secret://${binding.secretName}` : "source: <inline>"
476
+ })), "q");
477
+ if (selected === null) {
478
+ return null;
479
+ }
480
+ return bindings[selected] ?? null;
481
+ }
482
+ async function configureServerAuthInteractively(rl, serverName, reauthOnly = false) {
483
+ const config = loadConfig();
484
+ const spec = getServerSpecOrThrow(config, serverName);
485
+ const existingBindings = listAuthBindings(spec);
486
+ let targetKind;
487
+ let targetKey;
488
+ let existingValue;
489
+ if (reauthOnly) {
490
+ const selected = await chooseAuthBinding(rl, existingBindings.map((binding) => ({
491
+ kind: binding.kind,
492
+ key: binding.key,
493
+ value: binding.value,
494
+ secretName: secretRefName(binding.value) ?? undefined
495
+ })), "Choose binding to re-authenticate");
496
+ if (!selected) {
497
+ process.stdout.write("No auth binding selected.\n");
498
+ return;
499
+ }
500
+ targetKind = selected.kind;
501
+ targetKey = selected.key;
502
+ existingValue = selected.value;
503
+ }
504
+ else if (spec.transport === "http") {
505
+ targetKind = "header";
506
+ targetKey = await promptLineWithDefault(rl, "Header name", existingBindings.find((binding) => binding.kind === "header")?.key ?? "Authorization");
507
+ }
508
+ else {
509
+ targetKind = "env";
510
+ targetKey = await promptLineWithDefault(rl, "Env var name", existingBindings.find((binding) => binding.kind === "env")?.key ?? "");
511
+ }
512
+ if (!targetKey) {
513
+ process.stdout.write("Auth key cannot be empty.\n");
514
+ return;
515
+ }
516
+ const target = targetKind === "header"
517
+ ? resolveAuthTarget(spec, targetKey, undefined)
518
+ : resolveAuthTarget(spec, undefined, targetKey);
519
+ if (!existingValue) {
520
+ existingValue = existingBindings.find((binding) => binding.kind === target.kind && binding.key === target.key)?.value;
521
+ }
522
+ const token = await promptLine(rl, "Auth value/token (blank to cancel): ");
523
+ if (!token) {
524
+ process.stdout.write("Cancelled.\n");
525
+ return;
526
+ }
527
+ const authValue = maybePrefixBearer(target, token, false);
528
+ const defaultSecret = secretRefName(existingValue ?? "") ?? defaultAuthSecretName(serverName, target);
529
+ const secretName = await promptLineWithDefault(rl, "Secret name", defaultSecret);
530
+ if (!secretName) {
531
+ process.stdout.write("Secret name cannot be empty.\n");
532
+ return;
533
+ }
534
+ const secrets = new SecretsManager();
535
+ secrets.setSecret(secretName, authValue);
536
+ applyAuthReference(spec, target, toSecretRef(secretName));
537
+ saveConfig(config);
538
+ process.stdout.write(`Configured auth for "${serverName}" at ${target.kind}:${target.key} using secret://${secretName}.\n`);
539
+ }
540
+ async function clearServerAuthInteractively(rl, serverName) {
541
+ const config = loadConfig();
542
+ const spec = getServerSpecOrThrow(config, serverName);
543
+ const bindings = listAuthBindings(spec).map((binding) => ({
544
+ kind: binding.kind,
545
+ key: binding.key,
546
+ value: binding.value,
547
+ secretName: secretRefName(binding.value) ?? undefined
548
+ }));
549
+ const selected = await chooseAuthBinding(rl, bindings, "Choose binding to clear");
550
+ if (!selected) {
551
+ process.stdout.write("No auth binding selected.\n");
552
+ return;
553
+ }
554
+ const removed = removeAuthReference(spec, {
555
+ kind: selected.kind,
556
+ key: selected.key
557
+ });
558
+ if (!removed) {
559
+ process.stdout.write(`No auth binding found for ${selected.kind}:${selected.key}.\n`);
560
+ return;
561
+ }
562
+ const secretName = secretRefName(removed);
563
+ if (secretName) {
564
+ const shouldDelete = (await promptLine(rl, `Delete keychain secret "${secretName}" too? (y/N): `)).toLowerCase();
565
+ if (shouldDelete === "y" || shouldDelete === "yes") {
566
+ new SecretsManager().removeSecret(secretName);
567
+ process.stdout.write(`Removed binding and deleted ${secretName}.\n`);
568
+ }
569
+ else {
570
+ process.stdout.write("Removed binding.\n");
571
+ }
572
+ }
573
+ else {
574
+ process.stdout.write("Removed inline binding.\n");
575
+ }
576
+ saveConfig(config);
577
+ }
578
+ async function reconnectGatewayAndSync() {
579
+ const config = loadConfig();
580
+ const secrets = new SecretsManager();
581
+ const restart = await restartDaemon(config, process.argv[1] ?? "", secrets);
582
+ process.stdout.write(`${restart.message} pid=${restart.pid} port=${restart.port}\n`);
583
+ const summary = syncAllClients(config, secrets);
584
+ printSyncSummary(summary, false);
585
+ }
586
+ async function disableServerInteractively(rl, serverName) {
587
+ const confirmation = await promptLine(rl, `Type "${serverName}" to disable this MCP (blank to cancel): `);
588
+ if (confirmation !== serverName) {
589
+ process.stdout.write("Cancelled.\n");
590
+ return false;
591
+ }
592
+ const config = loadConfig();
593
+ removeServer(config, serverName, false);
594
+ saveConfig(config);
595
+ process.stdout.write(`Removed server: ${serverName}\n`);
596
+ process.stdout.write("Auto-syncing managed gateway entries across all supported clients...\n");
597
+ await autoSyncManagedEntries(config);
598
+ return true;
599
+ }
600
+ function buildServerActionTitle(server) {
601
+ const lines = [
602
+ `${serverHealthEmoji(server)} ${server.name} MCP Server`,
603
+ `Transport: ${server.transport}`,
604
+ `Target: ${server.target}`,
605
+ "Synced configs:"
606
+ ];
607
+ if (server.authBindings.length > 0) {
608
+ lines.splice(3, 0, `Auth: 🔐 ${formatAuthSummary(server.authBindings)}`);
609
+ }
610
+ const synced = listSyncedClientLabels(server);
611
+ if (synced.length === 0) {
612
+ lines.push(" - ⚠️ none");
613
+ }
614
+ else {
615
+ for (const entry of synced) {
616
+ lines.push(` - ${entry}`);
617
+ }
618
+ }
619
+ return lines;
620
+ }
621
+ function buildServerMenuDetail(server) {
622
+ const managed = server.clients.filter((client) => client.managed);
623
+ const errorCount = managed.filter((client) => client.status === "ERROR").length;
624
+ const syncSummary = managed.length > 0
625
+ ? `✅ ${managed.length} synced config${managed.length === 1 ? "" : "s"}`
626
+ : "⚠️ not synced";
627
+ const errorSummary = errorCount > 0 ? ` • ❌ ${errorCount} error` : "";
628
+ const authSummary = server.authBindings.length > 0 ? "🔐 auth configured • " : "";
629
+ return `${authSummary}${syncSummary}${errorSummary}`;
630
+ }
631
+ async function runServerActionsMenu(rl, serverName) {
632
+ while (true) {
633
+ const report = loadStatusReport();
634
+ const server = report.servers.find((entry) => entry.name === serverName);
635
+ if (!server) {
636
+ process.stdout.write(`Server "${serverName}" no longer exists.\n`);
637
+ return;
638
+ }
639
+ const action = await promptMenuSelection(rl, buildServerActionTitle(server), [
640
+ { label: "🔐 Configure auth", detail: "Set or update auth binding for this MCP." },
641
+ { label: "♻️ Re-authenticate", detail: "Replace token for an existing auth binding." },
642
+ { label: "🧹 Clear authentication", detail: "Remove a specific auth binding." },
643
+ { label: "🔄 Reconnect", detail: "Restart daemon and sync all managed client entries." },
644
+ { label: "🚫 Disable", detail: "Remove this MCP and sync removals to clients." },
645
+ { label: "← Back", detail: "Return to MCP list." }
646
+ ], "q");
647
+ if (action === null || action === 5) {
648
+ return;
649
+ }
650
+ try {
651
+ if (action === 0) {
652
+ await configureServerAuthInteractively(rl, serverName, false);
653
+ }
654
+ else if (action === 1) {
655
+ await configureServerAuthInteractively(rl, serverName, true);
656
+ }
657
+ else if (action === 2) {
658
+ await clearServerAuthInteractively(rl, serverName);
659
+ }
660
+ else if (action === 3) {
661
+ await reconnectGatewayAndSync();
662
+ }
663
+ else if (action === 4) {
664
+ const removed = await disableServerInteractively(rl, serverName);
665
+ if (removed) {
666
+ return;
667
+ }
668
+ }
669
+ }
670
+ catch (error) {
671
+ process.stdout.write(`${error.message}\n`);
672
+ }
673
+ await promptLine(rl, "Press Enter to continue...");
674
+ }
675
+ }
676
+ async function runInteractiveStatusMenu() {
677
+ const rl = createInterface({
678
+ input: process.stdin,
679
+ output: process.stdout
680
+ });
681
+ try {
682
+ while (true) {
683
+ const report = loadStatusReport();
684
+ if (report.servers.length === 0) {
685
+ clearTerminalScreen();
686
+ process.stdout.write("mcpx status\n");
687
+ process.stdout.write(`Gateway: ${report.gatewayUrl}\n`);
688
+ process.stdout.write(`Daemon: ${daemonEmoji(report.daemon.running)} ${formatDaemonState(report.daemon)}\n`);
689
+ process.stdout.write("⚠️ No upstream servers configured.\n");
690
+ return;
691
+ }
692
+ const selection = await promptMenuSelection(rl, [
693
+ "mcpx status",
694
+ `Gateway: ${report.gatewayUrl}`,
695
+ `Daemon: ${daemonEmoji(report.daemon.running)} ${formatDaemonState(report.daemon)}`,
696
+ `MCP servers: ${report.upstreamCount}`,
697
+ "Choose an MCP to manage:"
698
+ ], report.servers.map((server) => ({
699
+ label: `${serverHealthEmoji(server)} ${server.name} (${server.transport})`,
700
+ detail: buildServerMenuDetail(server)
701
+ })), "q");
702
+ if (selection === null) {
703
+ return;
704
+ }
705
+ const server = report.servers[selection];
706
+ if (!server) {
707
+ continue;
708
+ }
709
+ await runServerActionsMenu(rl, server.name);
710
+ }
711
+ }
712
+ finally {
713
+ rl.close();
714
+ }
715
+ }
159
716
  function registerAddCommand(parent) {
160
717
  parent
161
718
  .command("add [values...]")
@@ -170,6 +727,7 @@ function registerAddCommand(parent) {
170
727
  const config = loadConfig();
171
728
  const parsed = parseAddServerSpec(values ?? [], options);
172
729
  addServer(config, parsed.name, parsed.spec, options.force ?? false);
730
+ await maybeAutoConfigureAuthForAddedServer(parsed.name, parsed.spec, new SecretsManager());
173
731
  saveConfig(config);
174
732
  process.stdout.write(`Added server: ${parsed.name} (${parsed.spec.transport})\n`);
175
733
  process.stdout.write("Auto-syncing managed gateway entries across all supported clients...\n");
@@ -236,33 +794,21 @@ function registerSyncCommand(program) {
236
794
  function registerStatusCommand(program) {
237
795
  program
238
796
  .command("status")
797
+ .option("--no-interactive", "Disable interactive status menu")
239
798
  .option("--json", "Output JSON")
240
- .description("Show gateway, daemon, and client sync status")
241
- .action((options) => {
242
- const config = loadConfig();
243
- const daemon = getDaemonStatus(config);
244
- const statusPayload = {
245
- gatewayUrl: `http://127.0.0.1:${config.gateway.port}/mcp`,
246
- daemon,
247
- clients: config.clients,
248
- upstreamCount: Object.keys(config.servers).length
249
- };
799
+ .description("Show gateway, daemon, MCP inventory, and client sync status")
800
+ .action(async (options) => {
801
+ const report = loadStatusReport();
250
802
  if (options.json) {
251
- process.stdout.write(`${JSON.stringify(statusPayload, null, 2)}\n`);
803
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
252
804
  return;
253
805
  }
254
- process.stdout.write(`Gateway URL: ${statusPayload.gatewayUrl}\n`);
255
- process.stdout.write(`Daemon: ${daemon.running ? `running (pid ${daemon.pid})` : "stopped"}\n`);
256
- process.stdout.write(`Upstream servers: ${statusPayload.upstreamCount}\n`);
257
- for (const client of VALID_CLIENTS) {
258
- const state = config.clients[client];
259
- if (!state) {
260
- process.stdout.write(`- ${client}: SKIPPED\n`);
261
- }
262
- else {
263
- process.stdout.write(`- ${client}: ${state.status}${state.message ? ` - ${state.message}` : ""}\n`);
264
- }
806
+ const interactive = (options.interactive ?? true) && process.stdin.isTTY && process.stdout.isTTY;
807
+ if (interactive) {
808
+ await runInteractiveStatusMenu();
809
+ return;
265
810
  }
811
+ printStatusReportText(report);
266
812
  });
267
813
  }
268
814
  function registerDoctorCommand(program) {
@@ -555,6 +1101,23 @@ function registerMcpCompat(program) {
555
1101
  registerListCommand(mcp);
556
1102
  }
557
1103
  export async function runCli(argv = process.argv) {
1104
+ // Extract raw args (without node and script path) for compatibility check
1105
+ const rawArgs = argv.slice(2);
1106
+ // Check for client-native compatibility patterns before Commander parses
1107
+ if (rawArgs.length > 0) {
1108
+ const compat = parseCompatibilityArgs(rawArgs);
1109
+ // If unsupported client detected, show error and exit
1110
+ if (compat.error && compat.client !== null) {
1111
+ process.stderr.write(`Error: ${compat.error}\n`);
1112
+ process.exit(1);
1113
+ }
1114
+ // If valid compatibility command, normalize and inject into add flow
1115
+ if (compat.normalizedArgs !== null) {
1116
+ // Replace argv with normalized args for the add command
1117
+ // mcpx add <normalizedArgs...>
1118
+ argv = [argv[0], argv[1], "add", ...compat.normalizedArgs];
1119
+ }
1120
+ }
558
1121
  const program = new Command();
559
1122
  program.name("mcpx").description("HTTP-first MCP gateway and multi-client installer").version(APP_VERSION);
560
1123
  registerAddCommand(program);