@kwonye/mcpx 0.1.67 → 0.1.73

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 (144) hide show
  1. package/dist/adapters/claude-desktop.d.ts +1 -0
  2. package/dist/adapters/claude-desktop.d.ts.map +1 -0
  3. package/dist/adapters/claude.d.ts +1 -0
  4. package/dist/adapters/claude.d.ts.map +1 -0
  5. package/dist/adapters/cline.d.ts +1 -0
  6. package/dist/adapters/cline.d.ts.map +1 -0
  7. package/dist/adapters/codex.d.ts +1 -0
  8. package/dist/adapters/codex.d.ts.map +1 -0
  9. package/dist/adapters/cursor.d.ts +1 -0
  10. package/dist/adapters/cursor.d.ts.map +1 -0
  11. package/dist/adapters/index.d.ts +1 -0
  12. package/dist/adapters/index.d.ts.map +1 -0
  13. package/dist/adapters/kiro.d.ts +1 -0
  14. package/dist/adapters/kiro.d.ts.map +1 -0
  15. package/dist/adapters/opencode.d.ts +1 -0
  16. package/dist/adapters/opencode.d.ts.map +1 -0
  17. package/dist/adapters/qwen.d.ts +1 -0
  18. package/dist/adapters/qwen.d.ts.map +1 -0
  19. package/dist/adapters/utils/index.d.ts +2 -0
  20. package/dist/adapters/utils/index.d.ts.map +1 -0
  21. package/dist/adapters/vscode.d.ts +1 -0
  22. package/dist/adapters/vscode.d.ts.map +1 -0
  23. package/dist/cli-7rggq938.js +3292 -0
  24. package/dist/cli-ezvj44tr.js +3292 -0
  25. package/dist/cli-q8de7zb0.js +12 -0
  26. package/dist/cli.d.ts +1 -0
  27. package/dist/cli.d.ts.map +1 -0
  28. package/dist/cli.js +1880 -1131
  29. package/dist/compat/claude.d.ts +1 -0
  30. package/dist/compat/claude.d.ts.map +1 -0
  31. package/dist/compat/codex.d.ts +1 -0
  32. package/dist/compat/codex.d.ts.map +1 -0
  33. package/dist/compat/index.d.ts +1 -0
  34. package/dist/compat/index.d.ts.map +1 -0
  35. package/dist/compat/qwen.d.ts +1 -0
  36. package/dist/compat/qwen.d.ts.map +1 -0
  37. package/dist/compat/unsupported.d.ts +1 -0
  38. package/dist/compat/unsupported.d.ts.map +1 -0
  39. package/dist/compat/vscode.d.ts +1 -0
  40. package/dist/compat/vscode.d.ts.map +1 -0
  41. package/dist/core/auth-probe.d.ts +1 -0
  42. package/dist/core/auth-probe.d.ts.map +1 -0
  43. package/dist/core/config.d.ts +1 -0
  44. package/dist/core/config.d.ts.map +1 -0
  45. package/dist/core/daemon.d.ts +1 -0
  46. package/dist/core/daemon.d.ts.map +1 -0
  47. package/dist/core/index.d.ts +1 -0
  48. package/dist/core/index.d.ts.map +1 -0
  49. package/dist/core/index.js +81 -24
  50. package/dist/core/managed-index.d.ts +1 -0
  51. package/dist/core/managed-index.d.ts.map +1 -0
  52. package/dist/core/paths.d.ts +2 -0
  53. package/dist/core/paths.d.ts.map +1 -0
  54. package/dist/core/registry.d.ts +1 -0
  55. package/dist/core/registry.d.ts.map +1 -0
  56. package/dist/core/secrets.d.ts +1 -0
  57. package/dist/core/secrets.d.ts.map +1 -0
  58. package/dist/core/server-auth.d.ts +1 -0
  59. package/dist/core/server-auth.d.ts.map +1 -0
  60. package/dist/core/status.d.ts +1 -0
  61. package/dist/core/status.d.ts.map +1 -0
  62. package/dist/core/sync.d.ts +1 -0
  63. package/dist/core/sync.d.ts.map +1 -0
  64. package/dist/core/update-manager.d.ts +1 -0
  65. package/dist/core/update-manager.d.ts.map +1 -0
  66. package/dist/core/update.d.ts +1 -0
  67. package/dist/core/update.d.ts.map +1 -0
  68. package/dist/gateway/server.d.ts +1 -0
  69. package/dist/gateway/server.d.ts.map +1 -0
  70. package/dist/types.d.ts +1 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +8 -10
  73. package/dist/util/fs.d.ts +1 -0
  74. package/dist/util/fs.d.ts.map +1 -0
  75. package/dist/version.d.ts +2 -1
  76. package/dist/version.d.ts.map +1 -0
  77. package/package.json +10 -11
  78. package/dist/adapters/claude-desktop.js +0 -115
  79. package/dist/adapters/claude-desktop.js.map +0 -1
  80. package/dist/adapters/claude.js +0 -131
  81. package/dist/adapters/claude.js.map +0 -1
  82. package/dist/adapters/cline.js +0 -135
  83. package/dist/adapters/cline.js.map +0 -1
  84. package/dist/adapters/codex.js +0 -125
  85. package/dist/adapters/codex.js.map +0 -1
  86. package/dist/adapters/cursor.js +0 -122
  87. package/dist/adapters/cursor.js.map +0 -1
  88. package/dist/adapters/index.js +0 -23
  89. package/dist/adapters/index.js.map +0 -1
  90. package/dist/adapters/kiro.js +0 -120
  91. package/dist/adapters/kiro.js.map +0 -1
  92. package/dist/adapters/opencode.js +0 -122
  93. package/dist/adapters/opencode.js.map +0 -1
  94. package/dist/adapters/qwen.js +0 -113
  95. package/dist/adapters/qwen.js.map +0 -1
  96. package/dist/adapters/utils/index.js +0 -134
  97. package/dist/adapters/utils/index.js.map +0 -1
  98. package/dist/adapters/vscode.js +0 -122
  99. package/dist/adapters/vscode.js.map +0 -1
  100. package/dist/cli.js.map +0 -1
  101. package/dist/compat/claude.js +0 -252
  102. package/dist/compat/claude.js.map +0 -1
  103. package/dist/compat/codex.js +0 -179
  104. package/dist/compat/codex.js.map +0 -1
  105. package/dist/compat/index.js +0 -108
  106. package/dist/compat/index.js.map +0 -1
  107. package/dist/compat/qwen.js +0 -316
  108. package/dist/compat/qwen.js.map +0 -1
  109. package/dist/compat/unsupported.js +0 -47
  110. package/dist/compat/unsupported.js.map +0 -1
  111. package/dist/compat/vscode.js +0 -164
  112. package/dist/compat/vscode.js.map +0 -1
  113. package/dist/core/auth-probe.js +0 -54
  114. package/dist/core/auth-probe.js.map +0 -1
  115. package/dist/core/config.js +0 -76
  116. package/dist/core/config.js.map +0 -1
  117. package/dist/core/daemon.js +0 -176
  118. package/dist/core/daemon.js.map +0 -1
  119. package/dist/core/index.js.map +0 -1
  120. package/dist/core/managed-index.js +0 -22
  121. package/dist/core/managed-index.js.map +0 -1
  122. package/dist/core/paths.js +0 -55
  123. package/dist/core/paths.js.map +0 -1
  124. package/dist/core/registry.js +0 -51
  125. package/dist/core/registry.js.map +0 -1
  126. package/dist/core/secrets.js +0 -108
  127. package/dist/core/secrets.js.map +0 -1
  128. package/dist/core/server-auth.js +0 -112
  129. package/dist/core/server-auth.js.map +0 -1
  130. package/dist/core/status.js +0 -65
  131. package/dist/core/status.js.map +0 -1
  132. package/dist/core/sync.js +0 -128
  133. package/dist/core/sync.js.map +0 -1
  134. package/dist/core/update-manager.js +0 -170
  135. package/dist/core/update-manager.js.map +0 -1
  136. package/dist/core/update.js +0 -111
  137. package/dist/core/update.js.map +0 -1
  138. package/dist/gateway/server.js +0 -995
  139. package/dist/gateway/server.js.map +0 -1
  140. package/dist/types.js.map +0 -1
  141. package/dist/util/fs.js +0 -34
  142. package/dist/util/fs.js.map +0 -1
  143. package/dist/version.js +0 -2
  144. package/dist/version.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,1254 +1,2003 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * mcpx CLI - HTTP-first Model Context Protocol gateway
4
- */
2
+ import {
3
+ APP_VERSION,
4
+ STATUS_CLIENTS,
5
+ SecretsManager,
6
+ addServer,
7
+ applyAuthReference,
8
+ buildStatusReport,
9
+ checkForUpdates,
10
+ defaultAuthSecretName,
11
+ getAdapters,
12
+ getConfigPath,
13
+ getDaemonStatus,
14
+ getManagedIndexPath,
15
+ getStagedCliPath,
16
+ getStagedUpdate,
17
+ listAuthBindings,
18
+ loadConfig,
19
+ loadManagedIndex,
20
+ maybePrefixBearer,
21
+ performRollback,
22
+ performUpdate,
23
+ probeHttpAuthRequirement,
24
+ readDaemonLogs,
25
+ readSecretValueFromStdin,
26
+ removeAuthReference,
27
+ removeServer,
28
+ resolveAuthTarget,
29
+ restartDaemon,
30
+ runBackgroundUpdate,
31
+ runDaemonForeground,
32
+ saveConfig,
33
+ secretRefName,
34
+ setServerEnabled,
35
+ startDaemon,
36
+ stopDaemon,
37
+ syncAllClients,
38
+ toSecretRef
39
+ } from "./cli-7rggq938.js";
40
+ import {
41
+ isServerEnabled
42
+ } from "./cli-q8de7zb0.js";
43
+
44
+ // src/cli.ts
5
45
  import { Command } from "commander";
6
46
  import fs from "node:fs";
7
47
  import { execFileSync } from "node:child_process";
8
48
  import { pathToFileURL } from "node:url";
9
49
  import { emitKeypressEvents } from "node:readline";
10
50
  import { createInterface } from "node:readline/promises";
11
- import { loadConfig, saveConfig } from "./core/config.js";
12
- import { addServer, removeServer, setServerEnabled } from "./core/registry.js";
13
- import { SecretsManager, readSecretValueFromStdin } from "./core/secrets.js";
14
- import { probeHttpAuthRequirement } from "./core/auth-probe.js";
15
- import { syncAllClients } from "./core/sync.js";
16
- import { isServerEnabled } from "./types.js";
17
- import { parseCompatibilityArgs } from "./compat/index.js";
18
- import { applyAuthReference, defaultAuthSecretName, listAuthBindings, maybePrefixBearer, removeAuthReference, resolveAuthTarget, secretRefName, toSecretRef } from "./core/server-auth.js";
19
- import { getAdapters } from "./adapters/index.js";
20
- import { getDaemonStatus, readDaemonLogs, restartDaemon, runDaemonForeground, startDaemon, stopDaemon } from "./core/daemon.js";
21
- import { getConfigPath, getManagedIndexPath } from "./core/paths.js";
22
- import { loadManagedIndex } from "./core/managed-index.js";
23
- import { STATUS_CLIENTS, buildStatusReport } from "./core/status.js";
24
- import { APP_VERSION } from "./version.js";
25
- import { getStagedCliPath, getStagedUpdate, checkForUpdates } from "./core/update.js";
26
- import { performUpdate, performRollback, runBackgroundUpdate } from "./core/update-manager.js";
27
- const VALID_CLIENTS = STATUS_CLIENTS;
51
+
52
+ // src/compat/claude.ts
53
+ function parseClaudeFlags(argv) {
54
+ const headers = [];
55
+ const envVars = [];
56
+ let hasScope = false;
57
+ let hasTransportSSE = false;
58
+ let hasJsonInput = false;
59
+ let hasDesktopImport = false;
60
+ const separatorIndex = argv.indexOf("--");
61
+ const flagRegion = separatorIndex === -1 ? argv : argv.slice(0, separatorIndex);
62
+ for (let i = 0;i < flagRegion.length; i++) {
63
+ const arg = flagRegion[i];
64
+ if (arg === "--header" || arg === "-H") {
65
+ const value = flagRegion[i + 1];
66
+ if (value && !value.startsWith("-")) {
67
+ headers.push(value);
68
+ i++;
69
+ } else {
70
+ headers.push("");
71
+ }
72
+ } else if (arg === "--env" || arg === "-e") {
73
+ const value = flagRegion[i + 1];
74
+ if (value && !value.startsWith("-")) {
75
+ envVars.push(value);
76
+ i++;
77
+ } else {
78
+ envVars.push("");
79
+ }
80
+ } else if (arg === "--scope") {
81
+ hasScope = true;
82
+ } else if (arg === "--transport") {
83
+ const value = flagRegion[i + 1];
84
+ if (value && value.toLowerCase() === "sse") {
85
+ hasTransportSSE = true;
86
+ }
87
+ i++;
88
+ } else if (arg === "--" || arg.startsWith("-")) {}
89
+ }
90
+ hasJsonInput = argv.some((arg) => arg === "--json" || arg.startsWith("{"));
91
+ hasDesktopImport = argv.some((arg) => arg === "--from-desktop" || arg.includes("claude_desktop_config"));
92
+ return {
93
+ headers,
94
+ envVars,
95
+ hasScope,
96
+ hasTransportSSE,
97
+ hasJsonInput,
98
+ hasDesktopImport
99
+ };
100
+ }
101
+ function extractPositionalArgs(argv) {
102
+ const separatorIndex = argv.indexOf("--");
103
+ if (separatorIndex === -1) {
104
+ const positional = [];
105
+ let skipNext = false;
106
+ for (let i = 0;i < argv.length; i++) {
107
+ if (skipNext) {
108
+ skipNext = false;
109
+ continue;
110
+ }
111
+ const arg = argv[i];
112
+ if (arg === "--header" || arg === "-H" || arg === "--env" || arg === "-e" || arg === "--transport" || arg === "--scope") {
113
+ skipNext = true;
114
+ continue;
115
+ }
116
+ if (arg.startsWith("-")) {
117
+ continue;
118
+ }
119
+ positional.push(arg);
120
+ }
121
+ return positional;
122
+ } else {
123
+ const beforeSeparator = argv.slice(0, separatorIndex);
124
+ const afterSeparator = argv.slice(separatorIndex + 1);
125
+ const positional = [];
126
+ let skipNext = false;
127
+ for (let i = 0;i < beforeSeparator.length; i++) {
128
+ if (skipNext) {
129
+ skipNext = false;
130
+ continue;
131
+ }
132
+ const arg = beforeSeparator[i];
133
+ if (arg === "--header" || arg === "-H" || arg === "--env" || arg === "-e" || arg === "--transport" || arg === "--scope") {
134
+ skipNext = true;
135
+ continue;
136
+ }
137
+ if (arg.startsWith("-")) {
138
+ continue;
139
+ }
140
+ positional.push(arg);
141
+ }
142
+ return [...positional, "--", ...afterSeparator];
143
+ }
144
+ }
145
+ function validateClaudeArgs(flags) {
146
+ if (flags.hasScope) {
147
+ return "Claude --scope is not supported. Use global `mcpx add` for project-wide servers.";
148
+ }
149
+ if (flags.hasTransportSSE) {
150
+ return "Claude --transport sse is not supported. Use `mcpx add <name> <url>` for HTTP/SSE servers.";
151
+ }
152
+ if (flags.hasJsonInput) {
153
+ return "Claude JSON import is not supported. Use `mcpx add` directly or `mcpx code --add-mcp '<json>'` for VS Code format.";
154
+ }
155
+ if (flags.hasDesktopImport) {
156
+ return "Claude desktop config import is not supported. Use `mcpx add` to add servers individually.";
157
+ }
158
+ return null;
159
+ }
160
+ function parseClaudeArgs(argv) {
161
+ if (argv.length === 0) {
162
+ return {
163
+ normalizedArgs: [],
164
+ error: "Usage: mcpx claude mcp add <name> <url|command> [options...]"
165
+ };
166
+ }
167
+ const flags = parseClaudeFlags(argv);
168
+ const validationError = validateClaudeArgs(flags);
169
+ if (validationError) {
170
+ return {
171
+ normalizedArgs: [],
172
+ error: validationError
173
+ };
174
+ }
175
+ const convertedHeaders = flags.headers.map((header) => {
176
+ const colonIndex = header.indexOf(":");
177
+ if (colonIndex > 0) {
178
+ const key = header.slice(0, colonIndex).trim();
179
+ const value = header.slice(colonIndex + 1).trim();
180
+ return `${key}=${value}`;
181
+ }
182
+ return header;
183
+ });
184
+ const positional = extractPositionalArgs(argv);
185
+ const separatorIndex = positional.indexOf("--");
186
+ if (separatorIndex === -1) {
187
+ if (positional.length < 2) {
188
+ return {
189
+ normalizedArgs: [],
190
+ error: "HTTP mode requires: claude mcp add <name> <url>"
191
+ };
192
+ }
193
+ const [name, url, ...rest] = positional;
194
+ const normalized = [name, url];
195
+ for (const header of convertedHeaders) {
196
+ normalized.push("--header", header);
197
+ }
198
+ if (rest.length > 0) {
199
+ normalized.push(...rest);
200
+ }
201
+ return { normalizedArgs: normalized };
202
+ } else {
203
+ const beforeSeparator = positional.slice(0, separatorIndex);
204
+ const afterSeparator = positional.slice(separatorIndex + 1);
205
+ if (beforeSeparator.length < 1) {
206
+ return {
207
+ normalizedArgs: [],
208
+ error: "stdio mode requires: claude mcp add <name> -- <command> [args...]"
209
+ };
210
+ }
211
+ if (afterSeparator.length === 0) {
212
+ return {
213
+ normalizedArgs: [],
214
+ error: "stdio mode requires a command after `--`: claude mcp add <name> -- <command> [args...]"
215
+ };
216
+ }
217
+ const name = beforeSeparator[0];
218
+ const normalized = [name];
219
+ if (afterSeparator.length > 0) {
220
+ normalized.push(afterSeparator[0]);
221
+ if (afterSeparator.length > 1) {
222
+ normalized.push(...afterSeparator.slice(1));
223
+ }
224
+ }
225
+ for (const envVar of flags.envVars) {
226
+ normalized.push("--env", envVar);
227
+ }
228
+ for (const header of convertedHeaders) {
229
+ normalized.push("--header", header);
230
+ }
231
+ return { normalizedArgs: normalized };
232
+ }
233
+ }
234
+
235
+ // src/compat/codex.ts
236
+ function parseCodexFlags(argv) {
237
+ const envVars = [];
238
+ let hasUrlFlag = false;
239
+ let hasHttpSemantics = false;
240
+ const separatorIndex = argv.indexOf("--");
241
+ const flagRegion = separatorIndex === -1 ? argv : argv.slice(0, separatorIndex);
242
+ for (let i = 0;i < flagRegion.length; i++) {
243
+ const arg = flagRegion[i];
244
+ if (arg === "--env" || arg === "-e") {
245
+ const value = flagRegion[i + 1];
246
+ if (value && !value.startsWith("-")) {
247
+ envVars.push(value);
248
+ i++;
249
+ } else {
250
+ envVars.push("");
251
+ }
252
+ } else if (arg === "--url") {
253
+ hasUrlFlag = true;
254
+ hasHttpSemantics = true;
255
+ } else if (arg === "--http") {
256
+ hasHttpSemantics = true;
257
+ } else if (arg.startsWith("-") && arg !== "--") {}
258
+ }
259
+ return {
260
+ envVars,
261
+ hasUrlFlag,
262
+ hasHttpSemantics
263
+ };
264
+ }
265
+ function extractCodexPositionalArgs(argv) {
266
+ const separatorIndex = argv.indexOf("--");
267
+ if (separatorIndex === -1) {
268
+ const positional = [];
269
+ let skipNext = false;
270
+ for (let i = 0;i < argv.length; i++) {
271
+ if (skipNext) {
272
+ skipNext = false;
273
+ continue;
274
+ }
275
+ const arg = argv[i];
276
+ if (arg === "--env" || arg === "-e" || arg === "--url" || arg === "--http") {
277
+ skipNext = true;
278
+ continue;
279
+ }
280
+ if (arg.startsWith("-")) {
281
+ continue;
282
+ }
283
+ positional.push(arg);
284
+ }
285
+ return positional;
286
+ } else {
287
+ const beforeSeparator = argv.slice(0, separatorIndex);
288
+ const afterSeparator = argv.slice(separatorIndex + 1);
289
+ const positional = [];
290
+ let skipNext = false;
291
+ for (let i = 0;i < beforeSeparator.length; i++) {
292
+ if (skipNext) {
293
+ skipNext = false;
294
+ continue;
295
+ }
296
+ const arg = beforeSeparator[i];
297
+ if (arg === "--env" || arg === "-e" || arg === "--url" || arg === "--http") {
298
+ skipNext = true;
299
+ continue;
300
+ }
301
+ if (arg.startsWith("-")) {
302
+ continue;
303
+ }
304
+ positional.push(arg);
305
+ }
306
+ return [...positional, "--", ...afterSeparator];
307
+ }
308
+ }
309
+ function validateCodexArgs(flags, hasSeparator) {
310
+ if (flags.hasHttpSemantics) {
311
+ return "Codex HTTP semantics are not supported. Use `mcpx add <name> <url>` for HTTP MCP servers.";
312
+ }
313
+ if (!hasSeparator) {
314
+ return "Codex stdio mode requires `--` separator: codex mcp add <name> [--env KEY=VALUE] -- <command> [args...]";
315
+ }
316
+ return null;
317
+ }
318
+ function parseCodexArgs(argv) {
319
+ if (argv.length === 0) {
320
+ return {
321
+ normalizedArgs: [],
322
+ error: "Usage: mcpx codex mcp add <name> [--env KEY=VALUE ...] -- <command> [args...]"
323
+ };
324
+ }
325
+ const flags = parseCodexFlags(argv);
326
+ const hasSeparator = argv.includes("--");
327
+ const validationError = validateCodexArgs(flags, hasSeparator);
328
+ if (validationError) {
329
+ return {
330
+ normalizedArgs: [],
331
+ error: validationError
332
+ };
333
+ }
334
+ const positional = extractCodexPositionalArgs(argv);
335
+ const separatorIndex = positional.indexOf("--");
336
+ const beforeSeparator = positional.slice(0, separatorIndex);
337
+ const afterSeparator = positional.slice(separatorIndex + 1);
338
+ if (beforeSeparator.length < 1) {
339
+ return {
340
+ normalizedArgs: [],
341
+ error: "stdio mode requires: codex mcp add <name> -- <command> [args...]"
342
+ };
343
+ }
344
+ if (afterSeparator.length === 0) {
345
+ return {
346
+ normalizedArgs: [],
347
+ error: "stdio mode requires a command after `--`: codex mcp add <name> -- <command> [args...]"
348
+ };
349
+ }
350
+ const name = beforeSeparator[0];
351
+ const normalized = [name];
352
+ if (afterSeparator.length > 0) {
353
+ normalized.push(afterSeparator[0]);
354
+ if (afterSeparator.length > 1) {
355
+ normalized.push(...afterSeparator.slice(1));
356
+ }
357
+ }
358
+ for (const envVar of flags.envVars) {
359
+ normalized.push("--env", envVar);
360
+ }
361
+ return { normalizedArgs: normalized };
362
+ }
363
+
364
+ // src/compat/vscode.ts
365
+ var VS_CODE_ONLY_FIELDS = [
366
+ "sandbox",
367
+ "scope",
368
+ "container",
369
+ "containerId",
370
+ "workspaceFolder",
371
+ "lifecycle",
372
+ "alwaysAllow",
373
+ "disabled"
374
+ ];
375
+ function validateVSCodeConfig(config) {
376
+ for (const field of VS_CODE_ONLY_FIELDS) {
377
+ if (config[field] !== undefined) {
378
+ return `VS Code field "${field}" is not supported. Use global \`mcpx add\` for advanced configuration.`;
379
+ }
380
+ }
381
+ if (!config.name) {
382
+ return "VS Code config must include 'name' field.";
383
+ }
384
+ if (!config.url && !config.command) {
385
+ return "VS Code config must include either 'url' (HTTP) or 'command' (stdio) field.";
386
+ }
387
+ if (config.url && config.command) {
388
+ return "VS Code config cannot have both 'url' and 'command'. Specify one transport type.";
389
+ }
390
+ if (config.type !== undefined && config.type !== "http" && config.type !== "stdio") {
391
+ return `Invalid 'type' value: ${config.type}. Must be 'http' or 'stdio'.`;
392
+ }
393
+ if (config.env !== undefined) {
394
+ if (typeof config.env !== "object" || config.env === null || Array.isArray(config.env)) {
395
+ return "'env' must be a key-value object.";
396
+ }
397
+ for (const [key, value] of Object.entries(config.env)) {
398
+ if (typeof value !== "string") {
399
+ return `'env["${key}"]' must be a string value.`;
400
+ }
401
+ }
402
+ }
403
+ if (config.headers !== undefined) {
404
+ if (typeof config.headers !== "object" || config.headers === null || Array.isArray(config.headers)) {
405
+ return "'headers' must be a key-value object.";
406
+ }
407
+ for (const [key, value] of Object.entries(config.headers)) {
408
+ if (typeof value !== "string") {
409
+ return `'headers["${key}"]' must be a string value.`;
410
+ }
411
+ }
412
+ }
413
+ if (config.args !== undefined) {
414
+ if (!Array.isArray(config.args)) {
415
+ return "'args' must be an array.";
416
+ }
417
+ for (let i = 0;i < config.args.length; i++) {
418
+ if (typeof config.args[i] !== "string") {
419
+ return `'args[${i}]' must be a string.`;
420
+ }
421
+ }
422
+ }
423
+ return null;
424
+ }
425
+ function normalizeVSCodeConfig(config) {
426
+ const normalized = [config.name];
427
+ if (config.url) {
428
+ normalized.push(config.url);
429
+ if (config.headers) {
430
+ for (const [key, value] of Object.entries(config.headers)) {
431
+ normalized.push("--header", `${key}=${value}`);
432
+ }
433
+ }
434
+ } else if (config.command) {
435
+ normalized.push(config.command);
436
+ if (config.args && config.args.length > 0) {
437
+ normalized.push(...config.args);
438
+ }
439
+ if (config.env) {
440
+ for (const [key, value] of Object.entries(config.env)) {
441
+ normalized.push("--env", `${key}=${value}`);
442
+ }
443
+ }
444
+ }
445
+ return normalized;
446
+ }
447
+ function parseVSCodeArgs(jsonPayload) {
448
+ if (!jsonPayload) {
449
+ return {
450
+ normalizedArgs: [],
451
+ error: "Usage: mcpx code --add-mcp '<json>'"
452
+ };
453
+ }
454
+ let config;
455
+ try {
456
+ config = JSON.parse(jsonPayload);
457
+ } catch (parseError) {
458
+ return {
459
+ normalizedArgs: [],
460
+ error: `Invalid JSON: ${parseError.message}`
461
+ };
462
+ }
463
+ if (Array.isArray(config)) {
464
+ return {
465
+ normalizedArgs: [],
466
+ error: "Array of servers not supported. Use `mcpx add` for each server individually."
467
+ };
468
+ }
469
+ const validationError = validateVSCodeConfig(config);
470
+ if (validationError) {
471
+ return {
472
+ normalizedArgs: [],
473
+ error: validationError
474
+ };
475
+ }
476
+ const normalized = normalizeVSCodeConfig(config);
477
+ return { normalizedArgs: normalized };
478
+ }
479
+
480
+ // src/compat/qwen.ts
481
+ function parseQwenFlags(argv) {
482
+ const headers = [];
483
+ const envVars = [];
484
+ let transport;
485
+ let timeout;
486
+ let trust = false;
487
+ const includeTools = [];
488
+ const excludeTools = [];
489
+ let description;
490
+ let hasScope = false;
491
+ const separatorIndex = argv.indexOf("--");
492
+ const flagRegion = separatorIndex === -1 ? argv : argv.slice(0, separatorIndex);
493
+ for (let i = 0;i < flagRegion.length; i++) {
494
+ const arg = flagRegion[i];
495
+ if (arg === "--header" || arg === "-H") {
496
+ const value = flagRegion[i + 1];
497
+ if (value && !value.startsWith("-")) {
498
+ headers.push(value);
499
+ i++;
500
+ }
501
+ } else if (arg === "--env" || arg === "-e") {
502
+ const value = flagRegion[i + 1];
503
+ if (value && !value.startsWith("-")) {
504
+ envVars.push(value);
505
+ i++;
506
+ }
507
+ } else if (arg === "--transport" || arg === "-t") {
508
+ const value = flagRegion[i + 1];
509
+ if (value && (value === "stdio" || value === "http" || value === "sse")) {
510
+ transport = value;
511
+ }
512
+ i++;
513
+ } else if (arg === "--scope" || arg === "-s") {
514
+ hasScope = true;
515
+ i++;
516
+ } else if (arg === "--timeout") {
517
+ timeout = flagRegion[i + 1];
518
+ i++;
519
+ } else if (arg === "--trust") {
520
+ trust = true;
521
+ } else if (arg === "--include-tools") {
522
+ const value = flagRegion[i + 1];
523
+ if (value) {
524
+ includeTools.push(...value.split(",").map((t) => t.trim()));
525
+ i++;
526
+ }
527
+ } else if (arg === "--exclude-tools") {
528
+ const value = flagRegion[i + 1];
529
+ if (value) {
530
+ excludeTools.push(...value.split(",").map((t) => t.trim()));
531
+ i++;
532
+ }
533
+ } else if (arg === "--description") {
534
+ description = flagRegion[i + 1];
535
+ i++;
536
+ }
537
+ }
538
+ return {
539
+ headers,
540
+ envVars,
541
+ transport,
542
+ timeout,
543
+ trust,
544
+ includeTools,
545
+ excludeTools,
546
+ description,
547
+ hasScope
548
+ };
549
+ }
550
+ function extractPositionalArgs2(argv) {
551
+ const separatorIndex = argv.indexOf("--");
552
+ if (separatorIndex === -1) {
553
+ const positional = [];
554
+ let skipNext = false;
555
+ let foundCommand = false;
556
+ for (let i = 0;i < argv.length; i++) {
557
+ if (skipNext) {
558
+ skipNext = false;
559
+ continue;
560
+ }
561
+ const arg = argv[i];
562
+ if (arg === "--header" || arg === "-H" || arg === "--env" || arg === "-e" || arg === "--transport" || arg === "-t" || arg === "--scope" || arg === "-s" || arg === "--timeout" || arg === "--trust" || arg === "--include-tools" || arg === "--exclude-tools" || arg === "--description") {
563
+ skipNext = true;
564
+ continue;
565
+ }
566
+ if (!foundCommand && arg.startsWith("--") && !arg.includes("=")) {
567
+ continue;
568
+ }
569
+ if (!foundCommand && positional.length === 1) {
570
+ foundCommand = true;
571
+ }
572
+ positional.push(arg);
573
+ }
574
+ return positional;
575
+ } else {
576
+ const beforeSeparator = argv.slice(0, separatorIndex);
577
+ const afterSeparator = argv.slice(separatorIndex + 1);
578
+ const positional = [];
579
+ let skipNext = false;
580
+ let foundCommand = false;
581
+ for (let i = 0;i < beforeSeparator.length; i++) {
582
+ if (skipNext) {
583
+ skipNext = false;
584
+ continue;
585
+ }
586
+ const arg = beforeSeparator[i];
587
+ if (arg === "--header" || arg === "-H" || arg === "--env" || arg === "-e" || arg === "--transport" || arg === "-t" || arg === "--scope" || arg === "-s" || arg === "--timeout" || arg === "--trust" || arg === "--include-tools" || arg === "--exclude-tools" || arg === "--description") {
588
+ skipNext = true;
589
+ continue;
590
+ }
591
+ if (!foundCommand && arg.startsWith("--") && !arg.includes("=")) {
592
+ continue;
593
+ }
594
+ if (!foundCommand && positional.length === 1) {
595
+ foundCommand = true;
596
+ }
597
+ positional.push(arg);
598
+ }
599
+ return [...positional, "--", ...afterSeparator];
600
+ }
601
+ }
602
+ function validateQwenArgs(flags) {
603
+ if (flags.hasScope) {
604
+ return "Qwen --scope is not supported. Use global `mcpx add` for project-wide servers.";
605
+ }
606
+ if (flags.trust) {
607
+ return "Qwen --trust is not supported. Use `mcpx auth` to configure auth after adding the server.";
608
+ }
609
+ if (flags.includeTools.length > 0 || flags.excludeTools.length > 0) {
610
+ return "Qwen --include-tools/--exclude-tools are not supported. Use `mcpx add` directly for advanced configuration.";
611
+ }
612
+ if (flags.timeout) {
613
+ return "Qwen --timeout is not supported. Configure timeouts in the server config after adding.";
614
+ }
615
+ if (flags.transport === "sse") {
616
+ return "Qwen --transport sse is not supported. Use `mcpx add <name> <url>` for HTTP/SSE servers.";
617
+ }
618
+ return null;
619
+ }
620
+ function parseQwenArgs(argv) {
621
+ if (argv.length === 0) {
622
+ return {
623
+ normalizedArgs: [],
624
+ error: "Usage: mcpx qwen mcp add <name> <commandOrUrl> [options...]"
625
+ };
626
+ }
627
+ const flags = parseQwenFlags(argv);
628
+ const validationError = validateQwenArgs(flags);
629
+ if (validationError) {
630
+ return {
631
+ normalizedArgs: [],
632
+ error: validationError
633
+ };
634
+ }
635
+ const convertedHeaders = flags.headers.map((header) => {
636
+ const colonIndex = header.indexOf(":");
637
+ if (colonIndex > 0) {
638
+ const key = header.slice(0, colonIndex).trim();
639
+ const value = header.slice(colonIndex + 1).trim();
640
+ return `${key}=${value}`;
641
+ }
642
+ return header;
643
+ });
644
+ const positional = extractPositionalArgs2(argv);
645
+ const separatorIndex = positional.indexOf("--");
646
+ const secondPositional = positional[1];
647
+ const isHttpUrl = secondPositional && (secondPositional.startsWith("http://") || secondPositional.startsWith("https://"));
648
+ const isHttpMode = flags.transport === "http" || flags.transport === undefined && isHttpUrl;
649
+ if (isHttpMode && separatorIndex === -1) {
650
+ if (positional.length < 2) {
651
+ return {
652
+ normalizedArgs: [],
653
+ error: "HTTP mode requires: qwen mcp add <name> <url>"
654
+ };
655
+ }
656
+ const [name, url, ...rest] = positional;
657
+ const normalized = [name, url];
658
+ for (const header of convertedHeaders) {
659
+ normalized.push("--header", header);
660
+ }
661
+ if (flags.transport === "http") {
662
+ normalized.push("--transport", "http");
663
+ }
664
+ if (rest.length > 0) {
665
+ normalized.push(...rest);
666
+ }
667
+ return { normalizedArgs: normalized };
668
+ } else {
669
+ const beforeSeparator = separatorIndex === -1 ? positional : positional.slice(0, separatorIndex);
670
+ const afterSeparator = separatorIndex === -1 ? [] : positional.slice(separatorIndex + 1);
671
+ if (beforeSeparator.length < 1) {
672
+ return {
673
+ normalizedArgs: [],
674
+ error: "stdio mode requires: qwen mcp add <name> <command> [args...]"
675
+ };
676
+ }
677
+ const name = beforeSeparator[0];
678
+ const command = beforeSeparator[1];
679
+ if (!command) {
680
+ return {
681
+ normalizedArgs: [],
682
+ error: "stdio mode requires a command: qwen mcp add <name> <command> [args...]"
683
+ };
684
+ }
685
+ const normalized = [name, command];
686
+ const commandArgs = beforeSeparator.slice(2);
687
+ if (commandArgs.length > 0) {
688
+ normalized.push(...commandArgs);
689
+ }
690
+ if (afterSeparator.length > 0) {
691
+ normalized.push("--", ...afterSeparator);
692
+ }
693
+ for (const envVar of flags.envVars) {
694
+ normalized.push("--env", envVar);
695
+ }
696
+ for (const header of convertedHeaders) {
697
+ normalized.push("--header", header);
698
+ }
699
+ return { normalizedArgs: normalized };
700
+ }
701
+ }
702
+
703
+ // src/compat/unsupported.ts
704
+ var UNSUPPORTED_CLIENTS = {
705
+ "cursor-agent": "Cursor MCP management uses a different interface. Use `mcpx add <name> <url|command>` directly to add MCP servers to mcpx.",
706
+ cline: "Cline uses config-based MCP setup. Use `mcpx add <name> <url|command>` to add servers to mcpx, then sync to Cline.",
707
+ kiro: "Kiro uses one-click/config-based MCP setup. Use `mcpx add <name> <url|command>` to add servers to mcpx, then sync to Kiro.",
708
+ opencode: "OpenCode uses generic mcpx integration. Use `mcpx add <name> <url|command>` to add servers directly."
709
+ };
710
+ var UNSUPPORTED_PREFIXES = Object.keys(UNSUPPORTED_CLIENTS);
711
+ function detectUnsupportedClient(argv) {
712
+ if (argv.length === 0) {
713
+ return null;
714
+ }
715
+ const firstArg = argv[0];
716
+ for (const prefix of UNSUPPORTED_PREFIXES) {
717
+ if (firstArg === prefix || firstArg.startsWith(`${prefix}-`) || firstArg.startsWith(`${prefix}_`)) {
718
+ return {
719
+ client: firstArg,
720
+ error: `${UNSUPPORTED_CLIENTS[prefix]}
721
+
722
+ Example: mcpx add my-server https://example.com/mcp`
723
+ };
724
+ }
725
+ }
726
+ return null;
727
+ }
728
+
729
+ // src/compat/index.ts
730
+ function parseCompatibilityArgs(argv) {
731
+ const unsupported = detectUnsupportedClient(argv);
732
+ if (unsupported) {
733
+ return {
734
+ client: unsupported.client,
735
+ normalizedArgs: null,
736
+ error: unsupported.error
737
+ };
738
+ }
739
+ if (argv[0] === "claude" && argv[1] === "mcp" && argv[2] === "add") {
740
+ const claudeArgs = argv.slice(3);
741
+ const result = parseClaudeArgs(claudeArgs);
742
+ if (result.error) {
743
+ return {
744
+ client: "claude",
745
+ normalizedArgs: null,
746
+ error: result.error
747
+ };
748
+ }
749
+ return {
750
+ client: "claude",
751
+ normalizedArgs: result.normalizedArgs
752
+ };
753
+ }
754
+ if (argv[0] === "codex" && argv[1] === "mcp" && argv[2] === "add") {
755
+ const codexArgs = argv.slice(3);
756
+ const result = parseCodexArgs(codexArgs);
757
+ if (result.error) {
758
+ return {
759
+ client: "codex",
760
+ normalizedArgs: null,
761
+ error: result.error
762
+ };
763
+ }
764
+ return {
765
+ client: "codex",
766
+ normalizedArgs: result.normalizedArgs
767
+ };
768
+ }
769
+ if (argv[0] === "code" && argv[1] === "--add-mcp") {
770
+ const jsonPayload = argv[2];
771
+ const result = parseVSCodeArgs(jsonPayload);
772
+ if (result.error) {
773
+ return {
774
+ client: "vscode",
775
+ normalizedArgs: null,
776
+ error: result.error
777
+ };
778
+ }
779
+ return {
780
+ client: "vscode",
781
+ normalizedArgs: result.normalizedArgs
782
+ };
783
+ }
784
+ if (argv[0] === "qwen" && argv[1] === "mcp" && argv[2] === "add") {
785
+ const qwenArgs = argv.slice(3);
786
+ const result = parseQwenArgs(qwenArgs);
787
+ if (result.error) {
788
+ return {
789
+ client: "qwen",
790
+ normalizedArgs: null,
791
+ error: result.error
792
+ };
793
+ }
794
+ return {
795
+ client: "qwen",
796
+ normalizedArgs: result.normalizedArgs
797
+ };
798
+ }
799
+ return {
800
+ client: null,
801
+ normalizedArgs: null
802
+ };
803
+ }
804
+
805
+ // src/cli.ts
806
+ var VALID_CLIENTS = STATUS_CLIENTS;
28
807
  function parseKeyValueFlag(value, label) {
29
- const split = value.indexOf("=");
30
- if (split <= 0 || split >= value.length - 1) {
31
- throw new Error(`Invalid ${label} format: ${value}. Use KEY=VALUE.`);
32
- }
33
- return [value.slice(0, split), value.slice(split + 1)];
808
+ const split = value.indexOf("=");
809
+ if (split <= 0 || split >= value.length - 1) {
810
+ throw new Error(`Invalid ${label} format: ${value}. Use KEY=VALUE.`);
811
+ }
812
+ return [value.slice(0, split), value.slice(split + 1)];
34
813
  }
35
814
  function parseHeader(header) {
36
- return parseKeyValueFlag(header, "header");
815
+ return parseKeyValueFlag(header, "header");
37
816
  }
38
817
  function parseEnvVar(envVar) {
39
- return parseKeyValueFlag(envVar, "env");
818
+ return parseKeyValueFlag(envVar, "env");
40
819
  }
41
820
  function isHttpUrl(value) {
42
- try {
43
- const parsed = new URL(value);
44
- return parsed.protocol === "http:" || parsed.protocol === "https:";
45
- }
46
- catch {
47
- return false;
48
- }
821
+ try {
822
+ const parsed = new URL(value);
823
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
824
+ } catch {
825
+ return false;
826
+ }
49
827
  }
50
828
  function normalizeTransport(transport) {
51
- const normalized = transport?.trim().toLowerCase() || "auto";
52
- if (normalized !== "auto" && normalized !== "http" && normalized !== "stdio") {
53
- throw new Error("Invalid --transport value. Supported: auto, http, stdio.");
54
- }
55
- return normalized;
829
+ const normalized = transport?.trim().toLowerCase() || "auto";
830
+ if (normalized !== "auto" && normalized !== "http" && normalized !== "stdio") {
831
+ throw new Error("Invalid --transport value. Supported: auto, http, stdio.");
832
+ }
833
+ return normalized;
56
834
  }
57
835
  function parseAddServerSpec(values, options) {
58
- if (values.length < 2) {
59
- throw new Error("Usage: mcpx add [--transport auto|http|stdio] <name> <url|command> [args...]");
60
- }
61
- const name = values[0] ?? "";
62
- const target = values[1] ?? "";
63
- const trailing = values.slice(2);
64
- const requestedTransport = normalizeTransport(options.transport);
65
- const transport = requestedTransport === "auto"
66
- ? (isHttpUrl(target) && trailing.length === 0 ? "http" : "stdio")
67
- : requestedTransport;
68
- if (transport === "http") {
69
- if (values.length !== 2) {
70
- throw new Error("HTTP upstream usage: mcpx add <name> --transport http <url>");
71
- }
72
- if (!isHttpUrl(target)) {
73
- throw new Error(`Invalid HTTP URL: ${target}`);
74
- }
75
- if ((options.env ?? []).length > 0 || options.cwd) {
76
- throw new Error("--env/--cwd are only valid for stdio transport.");
77
- }
78
- const headers = {};
79
- for (const item of options.header ?? []) {
80
- const [key, value] = parseHeader(item);
81
- headers[key] = value;
82
- }
83
- const spec = {
84
- transport: "http",
85
- url: target,
86
- headers: Object.keys(headers).length > 0 ? headers : undefined
87
- };
88
- return { name, spec };
89
- }
90
- if ((options.header ?? []).length > 0) {
91
- throw new Error("--header is only valid for HTTP transport.");
92
- }
93
- const env = {};
94
- for (const item of options.env ?? []) {
95
- const [key, value] = parseEnvVar(item);
96
- env[key] = value;
97
- }
98
- const spec = {
99
- transport: "stdio",
100
- command: target,
101
- args: trailing.length > 0 ? trailing : undefined,
102
- env: Object.keys(env).length > 0 ? env : undefined,
103
- cwd: options.cwd?.trim() || undefined
836
+ if (values.length < 2) {
837
+ throw new Error("Usage: mcpx add [--transport auto|http|stdio] <name> <url|command> [args...]");
838
+ }
839
+ const name = values[0] ?? "";
840
+ const target = values[1] ?? "";
841
+ const trailing = values.slice(2);
842
+ const requestedTransport = normalizeTransport(options.transport);
843
+ const transport = requestedTransport === "auto" ? isHttpUrl(target) && trailing.length === 0 ? "http" : "stdio" : requestedTransport;
844
+ if (transport === "http") {
845
+ if (values.length !== 2) {
846
+ throw new Error("HTTP upstream usage: mcpx add <name> --transport http <url>");
847
+ }
848
+ if (!isHttpUrl(target)) {
849
+ throw new Error(`Invalid HTTP URL: ${target}`);
850
+ }
851
+ if ((options.env ?? []).length > 0 || options.cwd) {
852
+ throw new Error("--env/--cwd are only valid for stdio transport.");
853
+ }
854
+ const headers = {};
855
+ for (const item of options.header ?? []) {
856
+ const [key, value] = parseHeader(item);
857
+ headers[key] = value;
858
+ }
859
+ const spec2 = {
860
+ transport: "http",
861
+ url: target,
862
+ headers: Object.keys(headers).length > 0 ? headers : undefined
104
863
  };
105
- return { name, spec };
864
+ return { name, spec: spec2 };
865
+ }
866
+ if ((options.header ?? []).length > 0) {
867
+ throw new Error("--header is only valid for HTTP transport.");
868
+ }
869
+ const env = {};
870
+ for (const item of options.env ?? []) {
871
+ const [key, value] = parseEnvVar(item);
872
+ env[key] = value;
873
+ }
874
+ const spec = {
875
+ transport: "stdio",
876
+ command: target,
877
+ args: trailing.length > 0 ? trailing : undefined,
878
+ env: Object.keys(env).length > 0 ? env : undefined,
879
+ cwd: options.cwd?.trim() || undefined
880
+ };
881
+ return { name, spec };
106
882
  }
107
883
  function getServerSpecOrThrow(config, serverName) {
108
- const spec = config.servers[serverName];
109
- if (!spec) {
110
- throw new Error(`Server "${serverName}" does not exist.`);
111
- }
112
- return spec;
884
+ const spec = config.servers[serverName];
885
+ if (!spec) {
886
+ throw new Error(`Server "${serverName}" does not exist.`);
887
+ }
888
+ return spec;
113
889
  }
114
890
  function redactAuthValue(value) {
115
- if (value.startsWith("secret://")) {
116
- return value;
117
- }
118
- return "<inline>";
891
+ if (value.startsWith("secret://")) {
892
+ return value;
893
+ }
894
+ return "<inline>";
119
895
  }
120
896
  function parseClientList(values) {
121
- if (!values || values.length === 0) {
122
- return undefined;
123
- }
124
- const normalized = values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
125
- const unique = Array.from(new Set(normalized));
126
- for (const client of unique) {
127
- if (!VALID_CLIENTS.includes(client)) {
128
- throw new Error(`Unknown client: ${client}. Valid clients: ${VALID_CLIENTS.join(", ")}`);
129
- }
130
- }
131
- return unique;
897
+ if (!values || values.length === 0) {
898
+ return;
899
+ }
900
+ const normalized = values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
901
+ const unique = Array.from(new Set(normalized));
902
+ for (const client of unique) {
903
+ if (!VALID_CLIENTS.includes(client)) {
904
+ throw new Error(`Unknown client: ${client}. Valid clients: ${VALID_CLIENTS.join(", ")}`);
905
+ }
906
+ }
907
+ return unique;
132
908
  }
133
909
  function ensureExitCodeForSyncFailures(hasErrors) {
134
- if (hasErrors) {
135
- process.exitCode = 2;
136
- }
910
+ if (hasErrors) {
911
+ process.exitCode = 2;
912
+ }
137
913
  }
138
914
  function printSyncSummary(summary, asJson = false) {
139
- if (asJson) {
140
- process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
141
- return;
142
- }
143
- if (summary.imports.imported.length > 0
144
- || summary.imports.duplicates.length > 0
145
- || summary.imports.skipped.length > 0
146
- || summary.imports.conflicts.length > 0
147
- || summary.imports.errors.length > 0) {
148
- process.stdout.write("Imports:\n");
149
- for (const entry of summary.imports.imported) {
150
- process.stdout.write(`- imported ${entry.serverName} from ${entry.clientId}`);
151
- if (entry.sourceEntryName !== entry.serverName) {
152
- process.stdout.write(` (${entry.sourceEntryName})`);
153
- }
154
- process.stdout.write("\n");
155
- }
156
- for (const entry of summary.imports.duplicates) {
157
- process.stdout.write(`- adopted existing ${entry.serverName} from ${entry.clientId}\n`);
158
- }
159
- for (const entry of summary.imports.skipped) {
160
- process.stdout.write(`- skipped ${entry.serverName} from ${entry.clientId} - ${entry.reason}\n`);
161
- }
162
- for (const entry of summary.imports.conflicts) {
163
- process.stdout.write(`- conflict ${entry.serverName} from ${entry.clientId} - ${entry.message}\n`);
164
- }
165
- for (const entry of summary.imports.errors) {
166
- process.stdout.write(`- import error ${entry.clientId} - ${entry.message}\n`);
167
- }
168
- }
169
- process.stdout.write(`Gateway: ${summary.gatewayUrl}\n`);
170
- for (const result of summary.results) {
171
- process.stdout.write(`- ${result.clientId}: ${result.status}`);
172
- if (result.configPath) {
173
- process.stdout.write(` (${result.configPath})`);
174
- }
175
- if (result.message) {
176
- process.stdout.write(` - ${result.message}`);
177
- }
178
- process.stdout.write("\n");
179
- }
915
+ if (asJson) {
916
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}
917
+ `);
918
+ return;
919
+ }
920
+ if (summary.imports.imported.length > 0 || summary.imports.duplicates.length > 0 || summary.imports.skipped.length > 0 || summary.imports.conflicts.length > 0 || summary.imports.errors.length > 0) {
921
+ process.stdout.write(`Imports:
922
+ `);
923
+ for (const entry of summary.imports.imported) {
924
+ process.stdout.write(`- imported ${entry.serverName} from ${entry.clientId}`);
925
+ if (entry.sourceEntryName !== entry.serverName) {
926
+ process.stdout.write(` (${entry.sourceEntryName})`);
927
+ }
928
+ process.stdout.write(`
929
+ `);
930
+ }
931
+ for (const entry of summary.imports.duplicates) {
932
+ process.stdout.write(`- adopted existing ${entry.serverName} from ${entry.clientId}
933
+ `);
934
+ }
935
+ for (const entry of summary.imports.skipped) {
936
+ process.stdout.write(`- skipped ${entry.serverName} from ${entry.clientId} - ${entry.reason}
937
+ `);
938
+ }
939
+ for (const entry of summary.imports.conflicts) {
940
+ process.stdout.write(`- conflict ${entry.serverName} from ${entry.clientId} - ${entry.message}
941
+ `);
942
+ }
943
+ for (const entry of summary.imports.errors) {
944
+ process.stdout.write(`- import error ${entry.clientId} - ${entry.message}
945
+ `);
946
+ }
947
+ }
948
+ process.stdout.write(`Gateway: ${summary.gatewayUrl}
949
+ `);
950
+ for (const result of summary.results) {
951
+ process.stdout.write(`- ${result.clientId}: ${result.status}`);
952
+ if (result.configPath) {
953
+ process.stdout.write(` (${result.configPath})`);
954
+ }
955
+ if (result.message) {
956
+ process.stdout.write(` - ${result.message}`);
957
+ }
958
+ process.stdout.write(`
959
+ `);
960
+ }
180
961
  }
181
962
  async function ensureDaemonIfEnabled(cliPath, secrets) {
182
- if (process.env.MCPX_SKIP_DAEMON_AUTOSTART === "1") {
183
- return;
184
- }
185
- const config = loadConfig();
186
- if (!config.gateway.autoStart) {
187
- return;
188
- }
189
- await startDaemon(config, cliPath, secrets);
963
+ if (process.env.MCPX_SKIP_DAEMON_AUTOSTART === "1") {
964
+ return;
965
+ }
966
+ const config = loadConfig();
967
+ if (!config.gateway.autoStart) {
968
+ return;
969
+ }
970
+ await startDaemon(config, cliPath, secrets);
190
971
  }
191
972
  async function autoSyncManagedEntries(config) {
192
- const secrets = new SecretsManager();
193
- await ensureDaemonIfEnabled(process.argv[1] ?? "", secrets);
194
- const summary = syncAllClients(config, secrets);
195
- printSyncSummary(summary, false);
196
- ensureExitCodeForSyncFailures(summary.hasErrors);
973
+ const secrets = new SecretsManager;
974
+ await ensureDaemonIfEnabled(process.argv[1] ?? "", secrets);
975
+ const summary = syncAllClients(config, secrets);
976
+ printSyncSummary(summary, false);
977
+ ensureExitCodeForSyncFailures(summary.hasErrors);
197
978
  }
198
979
  function isNegativeChoice(value) {
199
- const normalized = value.trim().toLowerCase();
200
- return normalized === "n" || normalized === "no";
980
+ const normalized = value.trim().toLowerCase();
981
+ return normalized === "n" || normalized === "no";
201
982
  }
202
983
  async function maybeAutoConfigureAuthForAddedServer(serverName, spec, secrets) {
203
- if (spec.transport !== "http") {
204
- return;
205
- }
206
- const probe = await probeHttpAuthRequirement(spec, secrets);
207
- if (!probe.authRequired) {
208
- return;
209
- }
210
- process.stdout.write(`Upstream "${serverName}" responded with ${probe.status ?? 401} and appears to require auth.\n`);
211
- if (probe.wwwAuthenticate) {
212
- process.stdout.write(`WWW-Authenticate: ${probe.wwwAuthenticate}\n`);
213
- }
214
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
215
- process.stdout.write(`Run \`mcpx auth set ${serverName} --header Authorization --value \"<token>\"\` to configure auth.\n`);
216
- return;
217
- }
218
- const rl = createInterface({
219
- input: process.stdin,
220
- output: process.stdout
221
- });
222
- try {
223
- const shouldConfigure = await promptLine(rl, "Configure auth now? (Y/n): ");
224
- if (isNegativeChoice(shouldConfigure)) {
225
- process.stdout.write("Skipping auth setup.\n");
226
- return;
227
- }
228
- const currentHeaderBinding = listAuthBindings(spec)
229
- .find((binding) => binding.kind === "header");
230
- const headerName = await promptLineWithDefault(rl, "Header name", currentHeaderBinding?.key ?? "Authorization");
231
- if (!headerName) {
232
- process.stdout.write("Skipping auth setup (empty header name).\n");
233
- return;
234
- }
235
- const authValueInput = await promptLine(rl, "Auth value/token (blank to skip): ");
236
- if (!authValueInput) {
237
- process.stdout.write("Skipping auth setup.\n");
238
- return;
239
- }
240
- const target = resolveAuthTarget(spec, headerName, undefined);
241
- const authValue = maybePrefixBearer(target, authValueInput, false);
242
- const existingValue = listAuthBindings(spec)
243
- .find((binding) => binding.kind === target.kind && binding.key === target.key)?.value;
244
- const defaultSecret = secretRefName(existingValue ?? "") ?? defaultAuthSecretName(serverName, target);
245
- const secretName = await promptLineWithDefault(rl, "Secret name", defaultSecret);
246
- if (!secretName) {
247
- process.stdout.write("Skipping auth setup (empty secret name).\n");
248
- return;
249
- }
250
- secrets.setSecret(secretName, authValue);
251
- applyAuthReference(spec, target, toSecretRef(secretName));
252
- process.stdout.write(`Configured auth via ${target.kind}:${target.key} using secret://${secretName}.\n`);
253
- }
254
- catch (error) {
255
- process.stdout.write(`Auto auth setup skipped: ${error.message}\n`);
256
- return;
257
- }
258
- finally {
259
- rl.close();
260
- }
261
- const verify = await probeHttpAuthRequirement(spec, secrets);
262
- if (verify.authRequired) {
263
- process.stdout.write("Auth is configured, but upstream still reports auth required. Re-auth may be needed.\n");
264
- }
265
- else if (verify.error) {
266
- process.stdout.write(`Auth check after setup could not be completed: ${verify.error}\n`);
267
- }
268
- else {
269
- process.stdout.write("Auth check passed.\n");
984
+ if (spec.transport !== "http") {
985
+ return;
986
+ }
987
+ const probe = await probeHttpAuthRequirement(spec, secrets);
988
+ if (!probe.authRequired) {
989
+ return;
990
+ }
991
+ process.stdout.write(`Upstream "${serverName}" responded with ${probe.status ?? 401} and appears to require auth.
992
+ `);
993
+ if (probe.wwwAuthenticate) {
994
+ process.stdout.write(`WWW-Authenticate: ${probe.wwwAuthenticate}
995
+ `);
996
+ }
997
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
998
+ process.stdout.write(`Run \`mcpx auth set ${serverName} --header Authorization --value "<token>"\` to configure auth.
999
+ `);
1000
+ return;
1001
+ }
1002
+ const rl = createInterface({
1003
+ input: process.stdin,
1004
+ output: process.stdout
1005
+ });
1006
+ try {
1007
+ const shouldConfigure = await promptLine(rl, "Configure auth now? (Y/n): ");
1008
+ if (isNegativeChoice(shouldConfigure)) {
1009
+ process.stdout.write(`Skipping auth setup.
1010
+ `);
1011
+ return;
1012
+ }
1013
+ const currentHeaderBinding = listAuthBindings(spec).find((binding) => binding.kind === "header");
1014
+ const headerName = await promptLineWithDefault(rl, "Header name", currentHeaderBinding?.key ?? "Authorization");
1015
+ if (!headerName) {
1016
+ process.stdout.write(`Skipping auth setup (empty header name).
1017
+ `);
1018
+ return;
1019
+ }
1020
+ const authValueInput = await promptLine(rl, "Auth value/token (blank to skip): ");
1021
+ if (!authValueInput) {
1022
+ process.stdout.write(`Skipping auth setup.
1023
+ `);
1024
+ return;
1025
+ }
1026
+ const target = resolveAuthTarget(spec, headerName, undefined);
1027
+ const authValue = maybePrefixBearer(target, authValueInput, false);
1028
+ const existingValue = listAuthBindings(spec).find((binding) => binding.kind === target.kind && binding.key === target.key)?.value;
1029
+ const defaultSecret = secretRefName(existingValue ?? "") ?? defaultAuthSecretName(serverName, target);
1030
+ const secretName = await promptLineWithDefault(rl, "Secret name", defaultSecret);
1031
+ if (!secretName) {
1032
+ process.stdout.write(`Skipping auth setup (empty secret name).
1033
+ `);
1034
+ return;
270
1035
  }
1036
+ secrets.setSecret(secretName, authValue);
1037
+ applyAuthReference(spec, target, toSecretRef(secretName));
1038
+ process.stdout.write(`Configured auth via ${target.kind}:${target.key} using secret://${secretName}.
1039
+ `);
1040
+ } catch (error) {
1041
+ process.stdout.write(`Auto auth setup skipped: ${error.message}
1042
+ `);
1043
+ return;
1044
+ } finally {
1045
+ rl.close();
1046
+ }
1047
+ const verify = await probeHttpAuthRequirement(spec, secrets);
1048
+ if (verify.authRequired) {
1049
+ process.stdout.write(`Auth is configured, but upstream still reports auth required. Re-auth may be needed.
1050
+ `);
1051
+ } else if (verify.error) {
1052
+ process.stdout.write(`Auth check after setup could not be completed: ${verify.error}
1053
+ `);
1054
+ } else {
1055
+ process.stdout.write(`Auth check passed.
1056
+ `);
1057
+ }
271
1058
  }
272
1059
  function loadStatusReport() {
273
- const config = loadConfig();
274
- return buildStatusReport(config, loadManagedIndex(), getDaemonStatus(config));
1060
+ const config = loadConfig();
1061
+ return buildStatusReport(config, loadManagedIndex(), getDaemonStatus(config));
275
1062
  }
276
1063
  function daemonEmoji(running) {
277
- return running ? "✅" : "⚠️";
1064
+ return running ? "✅" : "⚠️";
278
1065
  }
279
1066
  function clientStatusEmoji(status) {
280
- if (status === "SYNCED") {
281
- return "✅";
282
- }
283
- if (status === "ERROR") {
284
- return "❌";
285
- }
286
- if (status === "UNSUPPORTED_HTTP") {
287
- return "⚠️";
288
- }
289
- return "⏭️";
1067
+ if (status === "SYNCED") {
1068
+ return "✅";
1069
+ }
1070
+ if (status === "ERROR") {
1071
+ return "❌";
1072
+ }
1073
+ if (status === "UNSUPPORTED_HTTP") {
1074
+ return "⚠️";
1075
+ }
1076
+ return "⏭️";
290
1077
  }
291
1078
  function serverHealthEmoji(server) {
292
- if (!server.enabled) {
293
- return "⏸️";
294
- }
295
- const managed = server.clients.filter((client) => client.managed);
296
- if (managed.length === 0) {
297
- return "⚠️";
298
- }
299
- if (managed.some((client) => client.status === "ERROR")) {
300
- return "❌";
301
- }
302
- if (managed.some((client) => client.status === "UNSUPPORTED_HTTP")) {
303
- return "⚠️";
304
- }
305
- return "✅";
1079
+ if (!server.enabled) {
1080
+ return "⏸️";
1081
+ }
1082
+ const managed = server.clients.filter((client) => client.managed);
1083
+ if (managed.length === 0) {
1084
+ return "⚠️";
1085
+ }
1086
+ if (managed.some((client) => client.status === "ERROR")) {
1087
+ return "❌";
1088
+ }
1089
+ if (managed.some((client) => client.status === "UNSUPPORTED_HTTP")) {
1090
+ return "⚠️";
1091
+ }
1092
+ return "✅";
306
1093
  }
307
1094
  function formatDaemonState(status) {
308
- if (status.running) {
309
- return `running (pid ${status.pid})`;
310
- }
311
- return "stopped";
1095
+ if (status.running) {
1096
+ return `running (pid ${status.pid})`;
1097
+ }
1098
+ return "stopped";
312
1099
  }
313
1100
  function formatAuthSummary(authBindings) {
314
- return authBindings.map((binding) => `${binding.kind}:${binding.key}`).join(", ");
1101
+ return authBindings.map((binding) => `${binding.kind}:${binding.key}`).join(", ");
315
1102
  }
316
1103
  function listSyncedClientLabels(server) {
317
- return server.clients
318
- .filter((client) => client.managed)
319
- .map((client) => (`${clientStatusEmoji(client.status)} ${client.configPath ? `${client.clientId} (${client.configPath})` : client.clientId}${client.status === "SYNCED" ? "" : ` [${client.status}]`}`));
1104
+ return server.clients.filter((client) => client.managed).map((client) => `${clientStatusEmoji(client.status)} ${client.configPath ? `${client.clientId} (${client.configPath})` : client.clientId}${client.status === "SYNCED" ? "" : ` [${client.status}]`}`);
320
1105
  }
321
1106
  function printSyncedConfigBullets(server, indent) {
322
- const synced = listSyncedClientLabels(server);
323
- if (synced.length === 0) {
324
- process.stdout.write(`${indent}- ⚠️ none\n`);
325
- return;
326
- }
327
- for (const entry of synced) {
328
- process.stdout.write(`${indent}- ${entry}\n`);
329
- }
1107
+ const synced = listSyncedClientLabels(server);
1108
+ if (synced.length === 0) {
1109
+ process.stdout.write(`${indent}- ⚠️ none
1110
+ `);
1111
+ return;
1112
+ }
1113
+ for (const entry of synced) {
1114
+ process.stdout.write(`${indent}- ${entry}
1115
+ `);
1116
+ }
330
1117
  }
331
1118
  function printStatusReportText(report) {
332
- process.stdout.write(`Gateway URL: ${report.gatewayUrl}\n`);
333
- process.stdout.write(`Daemon: ${daemonEmoji(report.daemon.running)} ${formatDaemonState(report.daemon)}\n`);
334
- process.stdout.write(`Upstream servers: ${report.upstreamCount}\n`);
335
- if (report.servers.length === 0) {
336
- process.stdout.write("⚠️ No upstream servers configured.\n");
337
- }
338
- else {
339
- for (const server of report.servers) {
340
- process.stdout.write(`- ${serverHealthEmoji(server)} ${server.name} (${server.transport})\n`);
341
- process.stdout.write(` state: ${server.enabled ? "enabled" : "disabled"}\n`);
342
- process.stdout.write(` target: ${server.target}\n`);
343
- if (server.authBindings.length > 0) {
344
- process.stdout.write(` auth: 🔐 ${formatAuthSummary(server.authBindings)}\n`);
345
- }
346
- process.stdout.write(" synced configs:\n");
347
- printSyncedConfigBullets(server, " ");
348
- }
349
- }
350
- process.stdout.write("Client sync states:\n");
351
- for (const client of STATUS_CLIENTS) {
352
- const state = report.clients[client];
353
- if (!state) {
354
- process.stdout.write(`- ${clientStatusEmoji("SKIPPED")} ${client}: SKIPPED\n`);
355
- }
356
- else {
357
- process.stdout.write(`- ${clientStatusEmoji(state.status)} ${client}: ${state.status}${state.message ? ` - ${state.message}` : ""}\n`);
358
- }
359
- }
1119
+ process.stdout.write(`Gateway URL: ${report.gatewayUrl}
1120
+ `);
1121
+ process.stdout.write(`Daemon: ${daemonEmoji(report.daemon.running)} ${formatDaemonState(report.daemon)}
1122
+ `);
1123
+ process.stdout.write(`Upstream servers: ${report.upstreamCount}
1124
+ `);
1125
+ if (report.servers.length === 0) {
1126
+ process.stdout.write(`⚠️ No upstream servers configured.
1127
+ `);
1128
+ } else {
1129
+ for (const server of report.servers) {
1130
+ process.stdout.write(`- ${serverHealthEmoji(server)} ${server.name} (${server.transport})
1131
+ `);
1132
+ process.stdout.write(` state: ${server.enabled ? "enabled" : "disabled"}
1133
+ `);
1134
+ process.stdout.write(` target: ${server.target}
1135
+ `);
1136
+ if (server.authBindings.length > 0) {
1137
+ process.stdout.write(` auth: \uD83D\uDD10 ${formatAuthSummary(server.authBindings)}
1138
+ `);
1139
+ }
1140
+ process.stdout.write(` synced configs:
1141
+ `);
1142
+ printSyncedConfigBullets(server, " ");
1143
+ }
1144
+ }
1145
+ process.stdout.write(`Client sync states:
1146
+ `);
1147
+ for (const client of STATUS_CLIENTS) {
1148
+ const state = report.clients[client];
1149
+ if (!state) {
1150
+ process.stdout.write(`- ${clientStatusEmoji("SKIPPED")} ${client}: SKIPPED
1151
+ `);
1152
+ } else {
1153
+ process.stdout.write(`- ${clientStatusEmoji(state.status)} ${client}: ${state.status}${state.message ? ` - ${state.message}` : ""}
1154
+ `);
1155
+ }
1156
+ }
360
1157
  }
361
1158
  async function promptLine(rl, prompt) {
362
- return (await rl.question(prompt)).trim();
1159
+ return (await rl.question(prompt)).trim();
363
1160
  }
364
1161
  async function promptLineWithDefault(rl, prompt, defaultValue) {
365
- const suffix = defaultValue ? ` [${defaultValue}]` : "";
366
- const value = (await rl.question(`${prompt}${suffix}: `)).trim();
367
- return value || defaultValue;
1162
+ const suffix = defaultValue ? ` [${defaultValue}]` : "";
1163
+ const value = (await rl.question(`${prompt}${suffix}: `)).trim();
1164
+ return value || defaultValue;
368
1165
  }
369
1166
  function parseSelection(value, max) {
370
- const numeric = Number(value);
371
- if (!Number.isInteger(numeric) || numeric < 1 || numeric > max) {
372
- return null;
373
- }
374
- return numeric - 1;
1167
+ const numeric = Number(value);
1168
+ if (!Number.isInteger(numeric) || numeric < 1 || numeric > max) {
1169
+ return null;
1170
+ }
1171
+ return numeric - 1;
375
1172
  }
376
1173
  function clearTerminalScreen() {
377
- process.stdout.write("\x1b[2J\x1b[H");
1174
+ process.stdout.write("\x1B[2J\x1B[H");
378
1175
  }
379
1176
  function renderMenuScreen(titleLines, options, selectedIndex, cancelHint) {
380
- clearTerminalScreen();
1177
+ clearTerminalScreen();
1178
+ for (const line of titleLines) {
1179
+ process.stdout.write(`${line}
1180
+ `);
1181
+ }
1182
+ process.stdout.write(`
1183
+ `);
1184
+ options.forEach((option, index) => {
1185
+ const cursor = index === selectedIndex ? "❯" : " ";
1186
+ process.stdout.write(`${cursor} ${index + 1}. ${option.label}
1187
+ `);
1188
+ if (option.detail) {
1189
+ process.stdout.write(` ${option.detail}
1190
+ `);
1191
+ }
1192
+ });
1193
+ process.stdout.write(`
1194
+ ↑/↓ to navigate • Space/Enter to select • Esc/${cancelHint} to cancel
1195
+ `);
1196
+ }
1197
+ async function promptMenuSelection(rl, titleLines, options, cancelHint = "q") {
1198
+ if (options.length === 0) {
1199
+ return null;
1200
+ }
1201
+ const stdin = process.stdin;
1202
+ const canUseKeyNavigation = process.stdin.isTTY && process.stdout.isTTY && typeof stdin.setRawMode === "function";
1203
+ if (!canUseKeyNavigation) {
381
1204
  for (const line of titleLines) {
382
- process.stdout.write(`${line}\n`);
1205
+ process.stdout.write(`${line}
1206
+ `);
383
1207
  }
384
- process.stdout.write("\n");
1208
+ process.stdout.write(`
1209
+ `);
385
1210
  options.forEach((option, index) => {
386
- const cursor = index === selectedIndex ? "❯" : " ";
387
- process.stdout.write(`${cursor} ${index + 1}. ${option.label}\n`);
388
- if (option.detail) {
389
- process.stdout.write(` ${option.detail}\n`);
390
- }
1211
+ process.stdout.write(`${index + 1}. ${option.label}
1212
+ `);
1213
+ if (option.detail) {
1214
+ process.stdout.write(` ${option.detail}
1215
+ `);
1216
+ }
391
1217
  });
392
- process.stdout.write(`\n↑/↓ to navigate • Space/Enter to select • Esc/${cancelHint} to cancel\n`);
393
- }
394
- async function promptMenuSelection(rl, titleLines, options, cancelHint = "q") {
395
- if (options.length === 0) {
396
- return null;
397
- }
398
- const stdin = process.stdin;
399
- const canUseKeyNavigation = process.stdin.isTTY && process.stdout.isTTY && typeof stdin.setRawMode === "function";
400
- if (!canUseKeyNavigation) {
401
- for (const line of titleLines) {
402
- process.stdout.write(`${line}\n`);
403
- }
404
- process.stdout.write("\n");
405
- options.forEach((option, index) => {
406
- process.stdout.write(`${index + 1}. ${option.label}\n`);
407
- if (option.detail) {
408
- process.stdout.write(` ${option.detail}\n`);
409
- }
410
- });
411
- const selected = parseSelection(await promptLine(rl, `Select option (1-${options.length}, blank to cancel): `), options.length);
412
- return selected;
413
- }
414
- return new Promise((resolve) => {
415
- let index = 0;
416
- let settled = false;
417
- rl.pause();
418
- const cleanup = () => {
419
- stdin.off("keypress", onKeypress);
420
- try {
421
- stdin.setRawMode(false);
422
- }
423
- catch {
424
- // ignore cleanup errors
425
- }
426
- rl.resume();
427
- };
428
- const done = (value) => {
429
- if (settled) {
430
- return;
431
- }
432
- settled = true;
433
- cleanup();
434
- process.stdout.write("\n");
435
- resolve(value);
436
- };
437
- const onKeypress = (_chunk, key) => {
438
- if (key.ctrl && key.name === "c") {
439
- done(null);
440
- return;
441
- }
442
- if (key.name === "up" || key.name === "k") {
443
- index = (index - 1 + options.length) % options.length;
444
- renderMenuScreen(titleLines, options, index, cancelHint);
445
- return;
446
- }
447
- if (key.name === "down" || key.name === "j") {
448
- index = (index + 1) % options.length;
449
- renderMenuScreen(titleLines, options, index, cancelHint);
450
- return;
451
- }
452
- if (key.name === "return" || key.name === "space") {
453
- done(index);
454
- return;
455
- }
456
- if (key.name === "escape" || key.name === "q") {
457
- done(null);
458
- return;
459
- }
460
- if (key.sequence && /^[1-9]$/.test(key.sequence)) {
461
- const numeric = Number(key.sequence);
462
- if (numeric >= 1 && numeric <= options.length) {
463
- done(numeric - 1);
464
- }
465
- }
466
- };
467
- emitKeypressEvents(stdin);
468
- stdin.setRawMode(true);
469
- stdin.resume();
470
- stdin.on("keypress", onKeypress);
1218
+ const selected = parseSelection(await promptLine(rl, `Select option (1-${options.length}, blank to cancel): `), options.length);
1219
+ return selected;
1220
+ }
1221
+ return new Promise((resolve) => {
1222
+ let index = 0;
1223
+ let settled = false;
1224
+ rl.pause();
1225
+ const cleanup = () => {
1226
+ stdin.off("keypress", onKeypress);
1227
+ try {
1228
+ stdin.setRawMode(false);
1229
+ } catch {}
1230
+ rl.resume();
1231
+ };
1232
+ const done = (value) => {
1233
+ if (settled) {
1234
+ return;
1235
+ }
1236
+ settled = true;
1237
+ cleanup();
1238
+ process.stdout.write(`
1239
+ `);
1240
+ resolve(value);
1241
+ };
1242
+ const onKeypress = (_chunk, key) => {
1243
+ if (key.ctrl && key.name === "c") {
1244
+ done(null);
1245
+ return;
1246
+ }
1247
+ if (key.name === "up" || key.name === "k") {
1248
+ index = (index - 1 + options.length) % options.length;
471
1249
  renderMenuScreen(titleLines, options, index, cancelHint);
472
- });
1250
+ return;
1251
+ }
1252
+ if (key.name === "down" || key.name === "j") {
1253
+ index = (index + 1) % options.length;
1254
+ renderMenuScreen(titleLines, options, index, cancelHint);
1255
+ return;
1256
+ }
1257
+ if (key.name === "return" || key.name === "space") {
1258
+ done(index);
1259
+ return;
1260
+ }
1261
+ if (key.name === "escape" || key.name === "q") {
1262
+ done(null);
1263
+ return;
1264
+ }
1265
+ if (key.sequence && /^[1-9]$/.test(key.sequence)) {
1266
+ const numeric = Number(key.sequence);
1267
+ if (numeric >= 1 && numeric <= options.length) {
1268
+ done(numeric - 1);
1269
+ }
1270
+ }
1271
+ };
1272
+ emitKeypressEvents(stdin);
1273
+ stdin.setRawMode(true);
1274
+ stdin.resume();
1275
+ stdin.on("keypress", onKeypress);
1276
+ renderMenuScreen(titleLines, options, index, cancelHint);
1277
+ });
473
1278
  }
474
1279
  async function chooseAuthBinding(rl, bindings, prompt) {
475
- if (bindings.length === 0) {
476
- return null;
477
- }
478
- if (bindings.length === 1) {
479
- return bindings[0];
480
- }
481
- const selected = await promptMenuSelection(rl, ["Auth bindings", prompt], bindings.map((binding) => ({
482
- label: `${binding.kind}:${binding.key}`,
483
- detail: binding.secretName ? `source: secret://${binding.secretName}` : "source: <inline>"
484
- })), "q");
485
- if (selected === null) {
486
- return null;
487
- }
488
- return bindings[selected] ?? null;
1280
+ if (bindings.length === 0) {
1281
+ return null;
1282
+ }
1283
+ if (bindings.length === 1) {
1284
+ return bindings[0];
1285
+ }
1286
+ const selected = await promptMenuSelection(rl, ["Auth bindings", prompt], bindings.map((binding) => ({
1287
+ label: `${binding.kind}:${binding.key}`,
1288
+ detail: binding.secretName ? `source: secret://${binding.secretName}` : "source: <inline>"
1289
+ })), "q");
1290
+ if (selected === null) {
1291
+ return null;
1292
+ }
1293
+ return bindings[selected] ?? null;
489
1294
  }
490
1295
  async function configureServerAuthInteractively(rl, serverName, reauthOnly = false) {
491
- const config = loadConfig();
492
- const spec = getServerSpecOrThrow(config, serverName);
493
- const existingBindings = listAuthBindings(spec);
494
- let targetKind;
495
- let targetKey;
496
- let existingValue;
497
- if (reauthOnly) {
498
- const selected = await chooseAuthBinding(rl, existingBindings.map((binding) => ({
499
- kind: binding.kind,
500
- key: binding.key,
501
- value: binding.value,
502
- secretName: secretRefName(binding.value) ?? undefined
503
- })), "Choose binding to re-authenticate");
504
- if (!selected) {
505
- process.stdout.write("No auth binding selected.\n");
506
- return;
507
- }
508
- targetKind = selected.kind;
509
- targetKey = selected.key;
510
- existingValue = selected.value;
511
- }
512
- else if (spec.transport === "http") {
513
- targetKind = "header";
514
- targetKey = await promptLineWithDefault(rl, "Header name", existingBindings.find((binding) => binding.kind === "header")?.key ?? "Authorization");
515
- }
516
- else {
517
- targetKind = "env";
518
- targetKey = await promptLineWithDefault(rl, "Env var name", existingBindings.find((binding) => binding.kind === "env")?.key ?? "");
519
- }
520
- if (!targetKey) {
521
- process.stdout.write("Auth key cannot be empty.\n");
522
- return;
523
- }
524
- const target = targetKind === "header"
525
- ? resolveAuthTarget(spec, targetKey, undefined)
526
- : resolveAuthTarget(spec, undefined, targetKey);
527
- if (!existingValue) {
528
- existingValue = existingBindings.find((binding) => binding.kind === target.kind && binding.key === target.key)?.value;
529
- }
530
- const token = await promptLine(rl, "Auth value/token (blank to cancel): ");
531
- if (!token) {
532
- process.stdout.write("Cancelled.\n");
533
- return;
534
- }
535
- const authValue = maybePrefixBearer(target, token, false);
536
- const defaultSecret = secretRefName(existingValue ?? "") ?? defaultAuthSecretName(serverName, target);
537
- const secretName = await promptLineWithDefault(rl, "Secret name", defaultSecret);
538
- if (!secretName) {
539
- process.stdout.write("Secret name cannot be empty.\n");
540
- return;
541
- }
542
- const secrets = new SecretsManager();
543
- secrets.setSecret(secretName, authValue);
544
- applyAuthReference(spec, target, toSecretRef(secretName));
545
- saveConfig(config);
546
- process.stdout.write(`Configured auth for "${serverName}" at ${target.kind}:${target.key} using secret://${secretName}.\n`);
1296
+ const config = loadConfig();
1297
+ const spec = getServerSpecOrThrow(config, serverName);
1298
+ const existingBindings = listAuthBindings(spec);
1299
+ let targetKind;
1300
+ let targetKey;
1301
+ let existingValue;
1302
+ if (reauthOnly) {
1303
+ const selected = await chooseAuthBinding(rl, existingBindings.map((binding) => ({
1304
+ kind: binding.kind,
1305
+ key: binding.key,
1306
+ value: binding.value,
1307
+ secretName: secretRefName(binding.value) ?? undefined
1308
+ })), "Choose binding to re-authenticate");
1309
+ if (!selected) {
1310
+ process.stdout.write(`No auth binding selected.
1311
+ `);
1312
+ return;
1313
+ }
1314
+ targetKind = selected.kind;
1315
+ targetKey = selected.key;
1316
+ existingValue = selected.value;
1317
+ } else if (spec.transport === "http") {
1318
+ targetKind = "header";
1319
+ targetKey = await promptLineWithDefault(rl, "Header name", existingBindings.find((binding) => binding.kind === "header")?.key ?? "Authorization");
1320
+ } else {
1321
+ targetKind = "env";
1322
+ targetKey = await promptLineWithDefault(rl, "Env var name", existingBindings.find((binding) => binding.kind === "env")?.key ?? "");
1323
+ }
1324
+ if (!targetKey) {
1325
+ process.stdout.write(`Auth key cannot be empty.
1326
+ `);
1327
+ return;
1328
+ }
1329
+ const target = targetKind === "header" ? resolveAuthTarget(spec, targetKey, undefined) : resolveAuthTarget(spec, undefined, targetKey);
1330
+ if (!existingValue) {
1331
+ existingValue = existingBindings.find((binding) => binding.kind === target.kind && binding.key === target.key)?.value;
1332
+ }
1333
+ const token = await promptLine(rl, "Auth value/token (blank to cancel): ");
1334
+ if (!token) {
1335
+ process.stdout.write(`Cancelled.
1336
+ `);
1337
+ return;
1338
+ }
1339
+ const authValue = maybePrefixBearer(target, token, false);
1340
+ const defaultSecret = secretRefName(existingValue ?? "") ?? defaultAuthSecretName(serverName, target);
1341
+ const secretName = await promptLineWithDefault(rl, "Secret name", defaultSecret);
1342
+ if (!secretName) {
1343
+ process.stdout.write(`Secret name cannot be empty.
1344
+ `);
1345
+ return;
1346
+ }
1347
+ const secrets = new SecretsManager;
1348
+ secrets.setSecret(secretName, authValue);
1349
+ applyAuthReference(spec, target, toSecretRef(secretName));
1350
+ saveConfig(config);
1351
+ process.stdout.write(`Configured auth for "${serverName}" at ${target.kind}:${target.key} using secret://${secretName}.
1352
+ `);
547
1353
  }
548
1354
  async function clearServerAuthInteractively(rl, serverName) {
549
- const config = loadConfig();
550
- const spec = getServerSpecOrThrow(config, serverName);
551
- const bindings = listAuthBindings(spec).map((binding) => ({
552
- kind: binding.kind,
553
- key: binding.key,
554
- value: binding.value,
555
- secretName: secretRefName(binding.value) ?? undefined
556
- }));
557
- const selected = await chooseAuthBinding(rl, bindings, "Choose binding to clear");
558
- if (!selected) {
559
- process.stdout.write("No auth binding selected.\n");
560
- return;
561
- }
562
- const removed = removeAuthReference(spec, {
563
- kind: selected.kind,
564
- key: selected.key
565
- });
566
- if (!removed) {
567
- process.stdout.write(`No auth binding found for ${selected.kind}:${selected.key}.\n`);
568
- return;
569
- }
570
- const secretName = secretRefName(removed);
571
- if (secretName) {
572
- const shouldDelete = (await promptLine(rl, `Delete keychain secret "${secretName}" too? (y/N): `)).toLowerCase();
573
- if (shouldDelete === "y" || shouldDelete === "yes") {
574
- new SecretsManager().removeSecret(secretName);
575
- process.stdout.write(`Removed binding and deleted ${secretName}.\n`);
576
- }
577
- else {
578
- process.stdout.write("Removed binding.\n");
579
- }
580
- }
581
- else {
582
- process.stdout.write("Removed inline binding.\n");
583
- }
584
- saveConfig(config);
1355
+ const config = loadConfig();
1356
+ const spec = getServerSpecOrThrow(config, serverName);
1357
+ const bindings = listAuthBindings(spec).map((binding) => ({
1358
+ kind: binding.kind,
1359
+ key: binding.key,
1360
+ value: binding.value,
1361
+ secretName: secretRefName(binding.value) ?? undefined
1362
+ }));
1363
+ const selected = await chooseAuthBinding(rl, bindings, "Choose binding to clear");
1364
+ if (!selected) {
1365
+ process.stdout.write(`No auth binding selected.
1366
+ `);
1367
+ return;
1368
+ }
1369
+ const removed = removeAuthReference(spec, {
1370
+ kind: selected.kind,
1371
+ key: selected.key
1372
+ });
1373
+ if (!removed) {
1374
+ process.stdout.write(`No auth binding found for ${selected.kind}:${selected.key}.
1375
+ `);
1376
+ return;
1377
+ }
1378
+ const secretName = secretRefName(removed);
1379
+ if (secretName) {
1380
+ const shouldDelete = (await promptLine(rl, `Delete keychain secret "${secretName}" too? (y/N): `)).toLowerCase();
1381
+ if (shouldDelete === "y" || shouldDelete === "yes") {
1382
+ new SecretsManager().removeSecret(secretName);
1383
+ process.stdout.write(`Removed binding and deleted ${secretName}.
1384
+ `);
1385
+ } else {
1386
+ process.stdout.write(`Removed binding.
1387
+ `);
1388
+ }
1389
+ } else {
1390
+ process.stdout.write(`Removed inline binding.
1391
+ `);
1392
+ }
1393
+ saveConfig(config);
585
1394
  }
586
1395
  async function reconnectGatewayAndSync() {
587
- const config = loadConfig();
588
- const secrets = new SecretsManager();
589
- const restart = await restartDaemon(config, process.argv[1] ?? "", secrets);
590
- process.stdout.write(`${restart.message} pid=${restart.pid} port=${restart.port}\n`);
591
- const summary = syncAllClients(config, secrets);
592
- printSyncSummary(summary, false);
1396
+ const config = loadConfig();
1397
+ const secrets = new SecretsManager;
1398
+ const restart = await restartDaemon(config, process.argv[1] ?? "", secrets);
1399
+ process.stdout.write(`${restart.message} pid=${restart.pid} port=${restart.port}
1400
+ `);
1401
+ const summary = syncAllClients(config, secrets);
1402
+ printSyncSummary(summary, false);
593
1403
  }
594
1404
  async function setServerEnabledInteractively(rl, serverName, enabled) {
595
- const action = enabled ? "enable" : "disable";
596
- const confirmation = await promptLine(rl, `Type "${serverName}" to ${action} this MCP (blank to cancel): `);
597
- if (confirmation !== serverName) {
598
- process.stdout.write("Cancelled.\n");
599
- return false;
600
- }
601
- const config = loadConfig();
602
- setServerEnabled(config, serverName, enabled);
603
- saveConfig(config);
604
- process.stdout.write(`${enabled ? "Enabled" : "Disabled"} server: ${serverName}\n`);
605
- process.stdout.write("Auto-syncing managed gateway entries across all supported clients...\n");
606
- await autoSyncManagedEntries(config);
607
- return true;
1405
+ const action = enabled ? "enable" : "disable";
1406
+ const confirmation = await promptLine(rl, `Type "${serverName}" to ${action} this MCP (blank to cancel): `);
1407
+ if (confirmation !== serverName) {
1408
+ process.stdout.write(`Cancelled.
1409
+ `);
1410
+ return false;
1411
+ }
1412
+ const config = loadConfig();
1413
+ setServerEnabled(config, serverName, enabled);
1414
+ saveConfig(config);
1415
+ process.stdout.write(`${enabled ? "Enabled" : "Disabled"} server: ${serverName}
1416
+ `);
1417
+ process.stdout.write(`Auto-syncing managed gateway entries across all supported clients...
1418
+ `);
1419
+ await autoSyncManagedEntries(config);
1420
+ return true;
608
1421
  }
609
1422
  function buildServerActionTitle(server) {
610
- const lines = [
611
- `${serverHealthEmoji(server)} ${server.name} MCP Server`,
612
- `State: ${server.enabled ? "enabled" : "disabled"}`,
613
- `Transport: ${server.transport}`,
614
- `Target: ${server.target}`,
615
- "Synced configs:"
616
- ];
617
- if (server.authBindings.length > 0) {
618
- lines.splice(3, 0, `Auth: 🔐 ${formatAuthSummary(server.authBindings)}`);
619
- }
620
- const synced = listSyncedClientLabels(server);
621
- if (synced.length === 0) {
622
- lines.push(" - ⚠️ none");
623
- }
624
- else {
625
- for (const entry of synced) {
626
- lines.push(` - ${entry}`);
627
- }
1423
+ const lines = [
1424
+ `${serverHealthEmoji(server)} ${server.name} MCP Server`,
1425
+ `State: ${server.enabled ? "enabled" : "disabled"}`,
1426
+ `Transport: ${server.transport}`,
1427
+ `Target: ${server.target}`,
1428
+ "Synced configs:"
1429
+ ];
1430
+ if (server.authBindings.length > 0) {
1431
+ lines.splice(3, 0, `Auth: \uD83D\uDD10 ${formatAuthSummary(server.authBindings)}`);
1432
+ }
1433
+ const synced = listSyncedClientLabels(server);
1434
+ if (synced.length === 0) {
1435
+ lines.push(" - ⚠️ none");
1436
+ } else {
1437
+ for (const entry of synced) {
1438
+ lines.push(` - ${entry}`);
628
1439
  }
629
- return lines;
1440
+ }
1441
+ return lines;
630
1442
  }
631
1443
  function buildServerMenuDetail(server) {
632
- if (!server.enabled) {
633
- return "⏸️ disabled";
634
- }
635
- const managed = server.clients.filter((client) => client.managed);
636
- const errorCount = managed.filter((client) => client.status === "ERROR").length;
637
- const syncSummary = managed.length > 0
638
- ? `✅ ${managed.length} synced config${managed.length === 1 ? "" : "s"}`
639
- : "⚠️ not synced";
640
- const errorSummary = errorCount > 0 ? ` • ❌ ${errorCount} error` : "";
641
- const authSummary = server.authBindings.length > 0 ? "🔐 auth configured • " : "";
642
- return `${authSummary}${syncSummary}${errorSummary}`;
1444
+ if (!server.enabled) {
1445
+ return "⏸️ disabled";
1446
+ }
1447
+ const managed = server.clients.filter((client) => client.managed);
1448
+ const errorCount = managed.filter((client) => client.status === "ERROR").length;
1449
+ const syncSummary = managed.length > 0 ? `✅ ${managed.length} synced config${managed.length === 1 ? "" : "s"}` : "⚠️ not synced";
1450
+ const errorSummary = errorCount > 0 ? ` • ❌ ${errorCount} error` : "";
1451
+ const authSummary = server.authBindings.length > 0 ? "\uD83D\uDD10 auth configured • " : "";
1452
+ return `${authSummary}${syncSummary}${errorSummary}`;
643
1453
  }
644
1454
  async function runServerActionsMenu(rl, serverName) {
645
- while (true) {
646
- const report = loadStatusReport();
647
- const server = report.servers.find((entry) => entry.name === serverName);
648
- if (!server) {
649
- process.stdout.write(`Server "${serverName}" no longer exists.\n`);
650
- return;
651
- }
652
- const action = await promptMenuSelection(rl, buildServerActionTitle(server), [
653
- { label: "🔐 Configure auth", detail: "Set or update auth binding for this MCP." },
654
- { label: "♻️ Re-authenticate", detail: "Replace token for an existing auth binding." },
655
- { label: "🧹 Clear authentication", detail: "Remove a specific auth binding." },
656
- { label: server.enabled ? "⏸️ Disable" : "▶️ Enable", detail: "Toggle this MCP across mcpx and all synced clients." },
657
- { label: "🔄 Reconnect", detail: "Restart daemon and sync all managed client entries." },
658
- { label: "← Back", detail: "Return to MCP list." }
659
- ], "q");
660
- if (action === null || action === 5) {
661
- return;
662
- }
663
- try {
664
- if (action === 0) {
665
- await configureServerAuthInteractively(rl, serverName, false);
666
- }
667
- else if (action === 1) {
668
- await configureServerAuthInteractively(rl, serverName, true);
669
- }
670
- else if (action === 2) {
671
- await clearServerAuthInteractively(rl, serverName);
672
- }
673
- else if (action === 3) {
674
- const updated = await setServerEnabledInteractively(rl, serverName, !server.enabled);
675
- if (updated) {
676
- return;
677
- }
678
- }
679
- else if (action === 4) {
680
- await reconnectGatewayAndSync();
681
- }
682
- }
683
- catch (error) {
684
- process.stdout.write(`${error.message}\n`);
685
- }
686
- await promptLine(rl, "Press Enter to continue...");
1455
+ while (true) {
1456
+ const report = loadStatusReport();
1457
+ const server = report.servers.find((entry) => entry.name === serverName);
1458
+ if (!server) {
1459
+ process.stdout.write(`Server "${serverName}" no longer exists.
1460
+ `);
1461
+ return;
1462
+ }
1463
+ const action = await promptMenuSelection(rl, buildServerActionTitle(server), [
1464
+ { label: "\uD83D\uDD10 Configure auth", detail: "Set or update auth binding for this MCP." },
1465
+ { label: "♻️ Re-authenticate", detail: "Replace token for an existing auth binding." },
1466
+ { label: "\uD83E\uDDF9 Clear authentication", detail: "Remove a specific auth binding." },
1467
+ { label: server.enabled ? "⏸️ Disable" : "▶️ Enable", detail: "Toggle this MCP across mcpx and all synced clients." },
1468
+ { label: "\uD83D\uDD04 Reconnect", detail: "Restart daemon and sync all managed client entries." },
1469
+ { label: "← Back", detail: "Return to MCP list." }
1470
+ ], "q");
1471
+ if (action === null || action === 5) {
1472
+ return;
687
1473
  }
1474
+ try {
1475
+ if (action === 0) {
1476
+ await configureServerAuthInteractively(rl, serverName, false);
1477
+ } else if (action === 1) {
1478
+ await configureServerAuthInteractively(rl, serverName, true);
1479
+ } else if (action === 2) {
1480
+ await clearServerAuthInteractively(rl, serverName);
1481
+ } else if (action === 3) {
1482
+ const updated = await setServerEnabledInteractively(rl, serverName, !server.enabled);
1483
+ if (updated) {
1484
+ return;
1485
+ }
1486
+ } else if (action === 4) {
1487
+ await reconnectGatewayAndSync();
1488
+ }
1489
+ } catch (error) {
1490
+ process.stdout.write(`${error.message}
1491
+ `);
1492
+ }
1493
+ await promptLine(rl, "Press Enter to continue...");
1494
+ }
688
1495
  }
689
1496
  async function runInteractiveStatusMenu() {
690
- const rl = createInterface({
691
- input: process.stdin,
692
- output: process.stdout
693
- });
694
- try {
695
- while (true) {
696
- const report = loadStatusReport();
697
- if (report.servers.length === 0) {
698
- clearTerminalScreen();
699
- process.stdout.write("mcpx status\n");
700
- process.stdout.write(`Gateway: ${report.gatewayUrl}\n`);
701
- process.stdout.write(`Daemon: ${daemonEmoji(report.daemon.running)} ${formatDaemonState(report.daemon)}\n`);
702
- process.stdout.write("⚠️ No upstream servers configured.\n");
703
- return;
704
- }
705
- const selection = await promptMenuSelection(rl, [
706
- "mcpx status",
707
- `Gateway: ${report.gatewayUrl}`,
708
- `Daemon: ${daemonEmoji(report.daemon.running)} ${formatDaemonState(report.daemon)}`,
709
- `MCP servers: ${report.upstreamCount}`,
710
- "Choose an MCP to manage:"
711
- ], report.servers.map((server) => ({
712
- label: `${serverHealthEmoji(server)} ${server.name} (${server.transport})`,
713
- detail: buildServerMenuDetail(server)
714
- })), "q");
715
- if (selection === null) {
716
- return;
717
- }
718
- const server = report.servers[selection];
719
- if (!server) {
720
- continue;
721
- }
722
- await runServerActionsMenu(rl, server.name);
723
- }
724
- }
725
- finally {
726
- rl.close();
727
- }
1497
+ const rl = createInterface({
1498
+ input: process.stdin,
1499
+ output: process.stdout
1500
+ });
1501
+ try {
1502
+ while (true) {
1503
+ const report = loadStatusReport();
1504
+ if (report.servers.length === 0) {
1505
+ clearTerminalScreen();
1506
+ process.stdout.write(`mcpx status
1507
+ `);
1508
+ process.stdout.write(`Gateway: ${report.gatewayUrl}
1509
+ `);
1510
+ process.stdout.write(`Daemon: ${daemonEmoji(report.daemon.running)} ${formatDaemonState(report.daemon)}
1511
+ `);
1512
+ process.stdout.write(`⚠️ No upstream servers configured.
1513
+ `);
1514
+ return;
1515
+ }
1516
+ const selection = await promptMenuSelection(rl, [
1517
+ "mcpx status",
1518
+ `Gateway: ${report.gatewayUrl}`,
1519
+ `Daemon: ${daemonEmoji(report.daemon.running)} ${formatDaemonState(report.daemon)}`,
1520
+ `MCP servers: ${report.upstreamCount}`,
1521
+ "Choose an MCP to manage:"
1522
+ ], report.servers.map((server2) => ({
1523
+ label: `${serverHealthEmoji(server2)} ${server2.name} (${server2.transport})`,
1524
+ detail: buildServerMenuDetail(server2)
1525
+ })), "q");
1526
+ if (selection === null) {
1527
+ return;
1528
+ }
1529
+ const server = report.servers[selection];
1530
+ if (!server) {
1531
+ continue;
1532
+ }
1533
+ await runServerActionsMenu(rl, server.name);
1534
+ }
1535
+ } finally {
1536
+ rl.close();
1537
+ }
728
1538
  }
729
1539
  function registerEnabledCommand(parent, enabled) {
730
- parent
731
- .command(enabled ? "enable <name>" : "disable <name>")
732
- .description(`${enabled ? "Enable" : "Disable"} an upstream MCP server`)
733
- .action(async (name) => {
734
- const config = loadConfig();
735
- setServerEnabled(config, name, enabled);
736
- saveConfig(config);
737
- process.stdout.write(`${enabled ? "Enabled" : "Disabled"} server: ${name}\n`);
738
- process.stdout.write("Auto-syncing managed gateway entries across all supported clients...\n");
739
- await autoSyncManagedEntries(config);
740
- });
1540
+ parent.command(enabled ? "enable <name>" : "disable <name>").description(`${enabled ? "Enable" : "Disable"} an upstream MCP server`).action(async (name) => {
1541
+ const config = loadConfig();
1542
+ setServerEnabled(config, name, enabled);
1543
+ saveConfig(config);
1544
+ process.stdout.write(`${enabled ? "Enabled" : "Disabled"} server: ${name}
1545
+ `);
1546
+ process.stdout.write(`Auto-syncing managed gateway entries across all supported clients...
1547
+ `);
1548
+ await autoSyncManagedEntries(config);
1549
+ });
741
1550
  }
742
1551
  function registerAddCommand(parent) {
743
- parent
744
- .command("add [values...]")
745
- .allowUnknownOption(true)
746
- .option("--transport <transport>", "MCP transport (auto|http|stdio)", "auto")
747
- .option("--header <header>", "Header entry in KEY=VALUE form", (value, previous = []) => [...previous, value], [])
748
- .option("--env <env>", "Env entry in KEY=VALUE form for stdio upstreams", (value, previous = []) => [...previous, value], [])
749
- .option("--cwd <cwd>", "Working directory for stdio upstream")
750
- .option("--force", "Overwrite existing server with same name")
751
- .description("Add an upstream MCP server")
752
- .action(async (values, options) => {
753
- const config = loadConfig();
754
- const parsed = parseAddServerSpec(values ?? [], options);
755
- addServer(config, parsed.name, parsed.spec, options.force ?? false);
756
- await maybeAutoConfigureAuthForAddedServer(parsed.name, parsed.spec, new SecretsManager());
757
- saveConfig(config);
758
- process.stdout.write(`Added server: ${parsed.name} (${parsed.spec.transport})\n`);
759
- process.stdout.write("Auto-syncing managed gateway entries across all supported clients...\n");
760
- await autoSyncManagedEntries(config);
761
- });
1552
+ parent.command("add [values...]").allowUnknownOption(true).option("--transport <transport>", "MCP transport (auto|http|stdio)", "auto").option("--header <header>", "Header entry in KEY=VALUE form", (value, previous = []) => [...previous, value], []).option("--env <env>", "Env entry in KEY=VALUE form for stdio upstreams", (value, previous = []) => [...previous, value], []).option("--cwd <cwd>", "Working directory for stdio upstream").option("--force", "Overwrite existing server with same name").description("Add an upstream MCP server").action(async (values, options) => {
1553
+ const config = loadConfig();
1554
+ const parsed = parseAddServerSpec(values ?? [], options);
1555
+ addServer(config, parsed.name, parsed.spec, options.force ?? false);
1556
+ await maybeAutoConfigureAuthForAddedServer(parsed.name, parsed.spec, new SecretsManager);
1557
+ saveConfig(config);
1558
+ process.stdout.write(`Added server: ${parsed.name} (${parsed.spec.transport})
1559
+ `);
1560
+ process.stdout.write(`Auto-syncing managed gateway entries across all supported clients...
1561
+ `);
1562
+ await autoSyncManagedEntries(config);
1563
+ });
762
1564
  }
763
1565
  function registerRemoveCommand(parent) {
764
- parent
765
- .command("remove <name>")
766
- .option("--force", "Do not error if server does not exist")
767
- .description("Remove an upstream MCP server")
768
- .action(async (name, options) => {
769
- const config = loadConfig();
770
- removeServer(config, name, options.force ?? false);
771
- saveConfig(config);
772
- process.stdout.write(`Removed server: ${name}\n`);
773
- process.stdout.write("Auto-syncing managed gateway entries across all supported clients...\n");
774
- await autoSyncManagedEntries(config);
775
- });
1566
+ parent.command("remove <name>").option("--force", "Do not error if server does not exist").description("Remove an upstream MCP server").action(async (name, options) => {
1567
+ const config = loadConfig();
1568
+ removeServer(config, name, options.force ?? false);
1569
+ saveConfig(config);
1570
+ process.stdout.write(`Removed server: ${name}
1571
+ `);
1572
+ process.stdout.write(`Auto-syncing managed gateway entries across all supported clients...
1573
+ `);
1574
+ await autoSyncManagedEntries(config);
1575
+ });
776
1576
  }
777
1577
  function registerListCommand(parent) {
778
- parent
779
- .command("list")
780
- .option("--json", "Output JSON")
781
- .description("List configured upstream MCP servers")
782
- .action((options) => {
783
- const config = loadConfig();
784
- const servers = Object.entries(config.servers).map(([name, spec]) => ({ name, ...spec }));
785
- if (options.json) {
786
- process.stdout.write(`${JSON.stringify({ servers }, null, 2)}\n`);
787
- return;
788
- }
789
- if (servers.length === 0) {
790
- process.stdout.write("No upstream servers configured.\n");
791
- return;
792
- }
793
- for (const server of servers) {
794
- const state = isServerEnabled(server) ? "enabled" : "disabled";
795
- if (server.transport === "http") {
796
- process.stdout.write(`- ${server.name} (${state}, http) ${server.url}\n`);
797
- }
798
- else {
799
- const args = (server.args ?? []).join(" ");
800
- process.stdout.write(`- ${server.name} (${state}, stdio) ${server.command}${args ? ` ${args}` : ""}\n`);
801
- }
802
- }
803
- });
1578
+ parent.command("list").option("--json", "Output JSON").description("List configured upstream MCP servers").action((options) => {
1579
+ const config = loadConfig();
1580
+ const servers = Object.entries(config.servers).map(([name, spec]) => ({ name, ...spec }));
1581
+ if (options.json) {
1582
+ process.stdout.write(`${JSON.stringify({ servers }, null, 2)}
1583
+ `);
1584
+ return;
1585
+ }
1586
+ if (servers.length === 0) {
1587
+ process.stdout.write(`No upstream servers configured.
1588
+ `);
1589
+ return;
1590
+ }
1591
+ for (const server of servers) {
1592
+ const state = isServerEnabled(server) ? "enabled" : "disabled";
1593
+ if (server.transport === "http") {
1594
+ process.stdout.write(`- ${server.name} (${state}, http) ${server.url}
1595
+ `);
1596
+ } else {
1597
+ const args = (server.args ?? []).join(" ");
1598
+ process.stdout.write(`- ${server.name} (${state}, stdio) ${server.command}${args ? ` ${args}` : ""}
1599
+ `);
1600
+ }
1601
+ }
1602
+ });
804
1603
  }
805
1604
  function registerSyncCommand(program) {
806
- program
807
- .command("sync [clients...]")
808
- .option("--client <id>", "Limit sync to specific client(s), comma-separated or repeated", (value, prev = []) => [...prev, value], [])
809
- .option("--json", "Output JSON")
810
- .description("Sync gateway configuration to supported clients (e.g. `mcpx sync claude`)")
811
- .action(async (clients, options) => {
812
- const config = loadConfig();
813
- const targetClients = parseClientList([...(clients ?? []), ...(options.client ?? [])]);
814
- const secrets = new SecretsManager();
815
- await ensureDaemonIfEnabled(process.argv[1] ?? "", secrets);
816
- const summary = syncAllClients(config, secrets, targetClients);
817
- printSyncSummary(summary, options.json ?? false);
818
- ensureExitCodeForSyncFailures(summary.hasErrors);
819
- });
1605
+ program.command("sync [clients...]").option("--client <id>", "Limit sync to specific client(s), comma-separated or repeated", (value, prev = []) => [...prev, value], []).option("--json", "Output JSON").description("Sync gateway configuration to supported clients (e.g. `mcpx sync claude`)").action(async (clients, options) => {
1606
+ const config = loadConfig();
1607
+ const targetClients = parseClientList([...clients ?? [], ...options.client ?? []]);
1608
+ const secrets = new SecretsManager;
1609
+ await ensureDaemonIfEnabled(process.argv[1] ?? "", secrets);
1610
+ const summary = syncAllClients(config, secrets, targetClients);
1611
+ printSyncSummary(summary, options.json ?? false);
1612
+ ensureExitCodeForSyncFailures(summary.hasErrors);
1613
+ });
820
1614
  }
821
1615
  function registerStatusCommand(program) {
822
- program
823
- .command("status")
824
- .option("--no-interactive", "Disable interactive status menu")
825
- .option("--json", "Output JSON")
826
- .description("Show gateway, daemon, MCP inventory, and client sync status")
827
- .action(async (options) => {
828
- const report = loadStatusReport();
829
- if (options.json) {
830
- process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
831
- return;
832
- }
833
- const interactive = (options.interactive ?? true) && process.stdin.isTTY && process.stdout.isTTY;
834
- if (interactive) {
835
- await runInteractiveStatusMenu();
836
- return;
837
- }
838
- printStatusReportText(report);
839
- });
1616
+ program.command("status").option("--no-interactive", "Disable interactive status menu").option("--json", "Output JSON").description("Show gateway, daemon, MCP inventory, and client sync status").action(async (options) => {
1617
+ const report = loadStatusReport();
1618
+ if (options.json) {
1619
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
1620
+ `);
1621
+ return;
1622
+ }
1623
+ const interactive = (options.interactive ?? true) && process.stdin.isTTY && process.stdout.isTTY;
1624
+ if (interactive) {
1625
+ await runInteractiveStatusMenu();
1626
+ return;
1627
+ }
1628
+ printStatusReportText(report);
1629
+ });
840
1630
  }
841
1631
  function registerDoctorCommand(program) {
842
- program
843
- .command("doctor")
844
- .option("--json", "Output JSON")
845
- .description("Run local health checks")
846
- .action((options) => {
847
- const config = loadConfig();
848
- const adapters = getAdapters();
849
- const checks = [];
850
- checks.push({
851
- check: "config_path",
852
- status: fs.existsSync(getConfigPath()) ? "ok" : "warn",
853
- details: getConfigPath()
854
- });
855
- checks.push({
856
- check: "managed_index_path",
857
- status: fs.existsSync(getManagedIndexPath()) ? "ok" : "warn",
858
- details: getManagedIndexPath()
859
- });
860
- if (process.platform === "darwin") {
861
- try {
862
- execFileSync("security", ["help"], { stdio: "ignore" });
863
- checks.push({ check: "macos_keychain", status: "ok", details: "security CLI available" });
864
- }
865
- catch {
866
- checks.push({ check: "macos_keychain", status: "fail", details: "security CLI unavailable" });
867
- }
868
- }
869
- else {
870
- checks.push({ check: "macos_keychain", status: "warn", details: "Non-macOS platform; keychain integration unavailable" });
871
- }
872
- const daemon = getDaemonStatus(config);
1632
+ program.command("doctor").option("--json", "Output JSON").description("Run local health checks").action((options) => {
1633
+ const config = loadConfig();
1634
+ const adapters = getAdapters();
1635
+ const checks = [];
1636
+ checks.push({
1637
+ check: "config_path",
1638
+ status: fs.existsSync(getConfigPath()) ? "ok" : "warn",
1639
+ details: getConfigPath()
1640
+ });
1641
+ checks.push({
1642
+ check: "managed_index_path",
1643
+ status: fs.existsSync(getManagedIndexPath()) ? "ok" : "warn",
1644
+ details: getManagedIndexPath()
1645
+ });
1646
+ if (process.platform === "darwin") {
1647
+ try {
1648
+ execFileSync("security", ["help"], { stdio: "ignore" });
1649
+ checks.push({ check: "macos_keychain", status: "ok", details: "security CLI available" });
1650
+ } catch {
1651
+ checks.push({ check: "macos_keychain", status: "fail", details: "security CLI unavailable" });
1652
+ }
1653
+ } else {
1654
+ checks.push({ check: "macos_keychain", status: "warn", details: "Non-macOS platform; keychain integration unavailable" });
1655
+ }
1656
+ const daemon = getDaemonStatus(config);
1657
+ checks.push({
1658
+ check: "daemon",
1659
+ status: daemon.running ? "ok" : "warn",
1660
+ details: daemon.running ? `pid ${daemon.pid}` : "not running"
1661
+ });
1662
+ for (const adapter of adapters) {
1663
+ const configPath = adapter.detectConfigPath();
1664
+ if (!configPath) {
873
1665
  checks.push({
874
- check: "daemon",
875
- status: daemon.running ? "ok" : "warn",
876
- details: daemon.running ? `pid ${daemon.pid}` : "not running"
1666
+ check: `client_${adapter.id}`,
1667
+ status: "warn",
1668
+ details: "no known config path"
877
1669
  });
878
- for (const adapter of adapters) {
879
- const configPath = adapter.detectConfigPath();
880
- if (!configPath) {
881
- checks.push({
882
- check: `client_${adapter.id}`,
883
- status: "warn",
884
- details: "no known config path"
885
- });
886
- continue;
887
- }
888
- checks.push({
889
- check: `client_${adapter.id}`,
890
- status: adapter.supportsHttp() ? "ok" : "warn",
891
- details: `${configPath}${adapter.supportsHttp() ? "" : " (HTTP unsupported)"}`
892
- });
893
- }
894
- if (options.json) {
895
- process.stdout.write(`${JSON.stringify({ checks }, null, 2)}\n`);
896
- return;
897
- }
898
- for (const item of checks) {
899
- process.stdout.write(`[${item.status.toUpperCase()}] ${item.check}: ${item.details}\n`);
900
- }
901
- if (checks.some((item) => item.status === "fail")) {
902
- process.exitCode = 1;
903
- }
904
- });
1670
+ continue;
1671
+ }
1672
+ checks.push({
1673
+ check: `client_${adapter.id}`,
1674
+ status: adapter.supportsHttp() ? "ok" : "warn",
1675
+ details: `${configPath}${adapter.supportsHttp() ? "" : " (HTTP unsupported)"}`
1676
+ });
1677
+ }
1678
+ if (options.json) {
1679
+ process.stdout.write(`${JSON.stringify({ checks }, null, 2)}
1680
+ `);
1681
+ return;
1682
+ }
1683
+ for (const item of checks) {
1684
+ process.stdout.write(`[${item.status.toUpperCase()}] ${item.check}: ${item.details}
1685
+ `);
1686
+ }
1687
+ if (checks.some((item) => item.status === "fail")) {
1688
+ process.exitCode = 1;
1689
+ }
1690
+ });
905
1691
  }
906
1692
  function registerDaemonCommands(program) {
907
- const daemon = program.command("daemon").description("Manage mcpx local gateway daemon");
908
- daemon
909
- .command("start")
910
- .description("Start background daemon")
911
- .action(async () => {
912
- const config = loadConfig();
913
- const result = await startDaemon(config, process.argv[1] ?? "", new SecretsManager());
914
- process.stdout.write(`${result.message} pid=${result.pid} port=${result.port}\n`);
915
- });
916
- daemon
917
- .command("stop")
918
- .description("Stop background daemon")
919
- .action(() => {
920
- const result = stopDaemon();
921
- process.stdout.write(`${result.message}\n`);
922
- });
923
- daemon
924
- .command("restart")
925
- .description("Restart background daemon")
926
- .action(async () => {
927
- const config = loadConfig();
928
- const result = await restartDaemon(config, process.argv[1] ?? "", new SecretsManager());
929
- process.stdout.write(`mcpx daemon restarted. pid=${result.pid} port=${result.port}\n`);
930
- });
931
- daemon
932
- .command("status")
933
- .description("Show daemon status")
934
- .action(() => {
935
- const status = getDaemonStatus(loadConfig());
936
- if (status.running) {
937
- process.stdout.write(`running pid=${status.pid} port=${status.port}\n`);
938
- }
939
- else {
940
- process.stdout.write("stopped\n");
941
- }
942
- });
943
- daemon
944
- .command("logs")
945
- .option("--lines <n>", "Number of lines", "200")
946
- .description("Show recent daemon logs")
947
- .action((options) => {
948
- const lines = Number(options.lines);
949
- process.stdout.write(`${readDaemonLogs(Number.isFinite(lines) ? lines : 200)}\n`);
950
- });
951
- daemon
952
- .command("run")
953
- .option("--port <port>", "Port override")
954
- .description("Run gateway in foreground (used by daemon manager)")
955
- .action(async (options) => {
956
- const config = loadConfig();
957
- const port = options.port ? Number(options.port) : config.gateway.port;
958
- if (!Number.isFinite(port) || port < 1 || port > 65535) {
959
- throw new Error(`Invalid port: ${options.port}`);
960
- }
961
- process.stdout.write(`Starting mcpx gateway on http://127.0.0.1:${port}/mcp\n`);
962
- await runDaemonForeground(config, port, new SecretsManager());
963
- });
1693
+ const daemon = program.command("daemon").description("Manage mcpx local gateway daemon");
1694
+ daemon.command("start").description("Start background daemon").action(async () => {
1695
+ const config = loadConfig();
1696
+ const result = await startDaemon(config, process.argv[1] ?? "", new SecretsManager);
1697
+ process.stdout.write(`${result.message} pid=${result.pid} port=${result.port}
1698
+ `);
1699
+ });
1700
+ daemon.command("stop").description("Stop background daemon").action(() => {
1701
+ const result = stopDaemon();
1702
+ process.stdout.write(`${result.message}
1703
+ `);
1704
+ });
1705
+ daemon.command("restart").description("Restart background daemon").action(async () => {
1706
+ const config = loadConfig();
1707
+ const result = await restartDaemon(config, process.argv[1] ?? "", new SecretsManager);
1708
+ process.stdout.write(`mcpx daemon restarted. pid=${result.pid} port=${result.port}
1709
+ `);
1710
+ });
1711
+ daemon.command("status").description("Show daemon status").action(() => {
1712
+ const status = getDaemonStatus(loadConfig());
1713
+ if (status.running) {
1714
+ process.stdout.write(`running pid=${status.pid} port=${status.port}
1715
+ `);
1716
+ } else {
1717
+ process.stdout.write(`stopped
1718
+ `);
1719
+ }
1720
+ });
1721
+ daemon.command("logs").option("--lines <n>", "Number of lines", "200").description("Show recent daemon logs").action((options) => {
1722
+ const lines = Number(options.lines);
1723
+ process.stdout.write(`${readDaemonLogs(Number.isFinite(lines) ? lines : 200)}
1724
+ `);
1725
+ });
1726
+ daemon.command("run").option("--port <port>", "Port override").description("Run gateway in foreground (used by daemon manager)").action(async (options) => {
1727
+ const config = loadConfig();
1728
+ const port = options.port ? Number(options.port) : config.gateway.port;
1729
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
1730
+ throw new Error(`Invalid port: ${options.port}`);
1731
+ }
1732
+ process.stdout.write(`Starting mcpx gateway on http://127.0.0.1:${port}/mcp
1733
+ `);
1734
+ await runDaemonForeground(config, port, new SecretsManager);
1735
+ });
964
1736
  }
965
1737
  function registerSecretsCommands(program) {
966
- const secret = program.command("secret").description("Manage keychain-backed secrets");
967
- secret
968
- .command("set <name>")
969
- .option("--value <value>", "Secret value")
970
- .description("Set secret in OS keychain")
971
- .action((name, options) => {
972
- const value = options.value ?? readSecretValueFromStdin();
973
- new SecretsManager().setSecret(name, value);
974
- process.stdout.write(`Secret set: ${name}\n`);
975
- });
976
- secret
977
- .command("rm <name>")
978
- .description("Remove secret from OS keychain")
979
- .action((name) => {
980
- new SecretsManager().removeSecret(name);
981
- process.stdout.write(`Secret removed: ${name}\n`);
982
- });
983
- secret
984
- .command("ls")
985
- .description("List secret names tracked by mcpx")
986
- .action(() => {
987
- const names = new SecretsManager().listSecretNames();
988
- if (names.length === 0) {
989
- process.stdout.write("No tracked secrets.\n");
990
- return;
991
- }
992
- for (const name of names) {
993
- process.stdout.write(`${name}\n`);
994
- }
995
- });
1738
+ const secret = program.command("secret").description("Manage keychain-backed secrets");
1739
+ secret.command("set <name>").option("--value <value>", "Secret value").description("Set secret in OS keychain").action((name, options) => {
1740
+ const value = options.value ?? readSecretValueFromStdin();
1741
+ new SecretsManager().setSecret(name, value);
1742
+ process.stdout.write(`Secret set: ${name}
1743
+ `);
1744
+ });
1745
+ secret.command("rm <name>").description("Remove secret from OS keychain").action((name) => {
1746
+ new SecretsManager().removeSecret(name);
1747
+ process.stdout.write(`Secret removed: ${name}
1748
+ `);
1749
+ });
1750
+ secret.command("ls").description("List secret names tracked by mcpx").action(() => {
1751
+ const names = new SecretsManager().listSecretNames();
1752
+ if (names.length === 0) {
1753
+ process.stdout.write(`No tracked secrets.
1754
+ `);
1755
+ return;
1756
+ }
1757
+ for (const name of names) {
1758
+ process.stdout.write(`${name}
1759
+ `);
1760
+ }
1761
+ });
996
1762
  }
997
1763
  function registerAuthCommands(program) {
998
- const auth = program.command("auth").description("Auth utilities");
999
- auth
1000
- .command("set <server>")
1001
- .option("--header <name>", "HTTP header name (default for HTTP servers: Authorization)")
1002
- .option("--env <name>", "Env var name (required for stdio servers)")
1003
- .option("--value <value>", "Auth value/token (if omitted, read from stdin)")
1004
- .option("--secret-name <name>", "Override keychain secret name")
1005
- .option("--raw", "Do not auto-prefix Authorization values with Bearer")
1006
- .description("Store auth in keychain and bind it to an upstream server")
1007
- .action((server, options) => {
1008
- const config = loadConfig();
1009
- const spec = getServerSpecOrThrow(config, server);
1010
- const target = resolveAuthTarget(spec, options.header, options.env);
1011
- const provided = options.value ?? readSecretValueFromStdin();
1012
- const authValue = maybePrefixBearer(target, provided, options.raw ?? false);
1013
- const secretName = options.secretName?.trim() || defaultAuthSecretName(server, target);
1014
- const secrets = new SecretsManager();
1015
- secrets.setSecret(secretName, authValue);
1016
- applyAuthReference(spec, target, toSecretRef(secretName));
1017
- saveConfig(config);
1018
- process.stdout.write(`Configured auth for server "${server}" via ${target.kind} "${target.key}".\n`);
1019
- process.stdout.write(`Stored secret: ${secretName}\n`);
1020
- });
1021
- auth
1022
- .command("rm <server>")
1023
- .option("--header <name>", "HTTP header name (default for HTTP servers: Authorization)")
1024
- .option("--env <name>", "Env var name (required for stdio servers)")
1025
- .option("--delete-secret", "Delete referenced keychain secret when removing binding")
1026
- .description("Remove auth binding from an upstream server")
1027
- .action((server, options) => {
1028
- const config = loadConfig();
1029
- const spec = getServerSpecOrThrow(config, server);
1030
- const target = resolveAuthTarget(spec, options.header, options.env);
1031
- const removedValue = removeAuthReference(spec, target);
1032
- if (!removedValue) {
1033
- process.stdout.write(`No auth binding found for server "${server}" at ${target.kind} "${target.key}".\n`);
1034
- return;
1035
- }
1036
- if (options.deleteSecret) {
1037
- const secretName = secretRefName(removedValue);
1038
- if (secretName) {
1039
- new SecretsManager().removeSecret(secretName);
1040
- process.stdout.write(`Removed binding and deleted secret: ${secretName}\n`);
1041
- }
1042
- else {
1043
- process.stdout.write("Removed binding. Value was inline (no keychain secret deleted).\n");
1044
- }
1045
- }
1046
- else {
1047
- process.stdout.write("Removed binding.\n");
1048
- }
1049
- saveConfig(config);
1050
- });
1051
- auth
1052
- .command("show [server]")
1053
- .option("--json", "Output JSON")
1054
- .description("Show configured upstream auth bindings (secret refs only, values redacted)")
1055
- .action((server, options) => {
1056
- const config = loadConfig();
1057
- const names = server ? [server] : Object.keys(config.servers);
1058
- const output = names.map((name) => {
1059
- const spec = getServerSpecOrThrow(config, name);
1060
- const bindings = listAuthBindings(spec).map((binding) => ({
1061
- kind: binding.kind,
1062
- key: binding.key,
1063
- value: redactAuthValue(binding.value)
1064
- }));
1065
- return {
1066
- server: name,
1067
- transport: spec.transport,
1068
- bindings
1069
- };
1070
- });
1071
- if (options.json) {
1072
- process.stdout.write(`${JSON.stringify({ auth: output }, null, 2)}\n`);
1073
- return;
1074
- }
1075
- if (output.length === 0) {
1076
- process.stdout.write("No upstream servers configured.\n");
1077
- return;
1078
- }
1079
- for (const serverEntry of output) {
1080
- process.stdout.write(`- ${serverEntry.server} (${serverEntry.transport})\n`);
1081
- if (serverEntry.bindings.length === 0) {
1082
- process.stdout.write(" (no auth bindings)\n");
1083
- continue;
1084
- }
1085
- for (const binding of serverEntry.bindings) {
1086
- process.stdout.write(` ${binding.kind}:${binding.key} = ${binding.value}\n`);
1087
- }
1088
- }
1089
- });
1090
- auth
1091
- .command("rotate-local-token")
1092
- .description("Rotate bearer token used for local client->mcpx auth")
1093
- .action(async () => {
1094
- const secrets = new SecretsManager();
1095
- secrets.rotateLocalToken("local_gateway_token");
1096
- const config = loadConfig();
1097
- await restartDaemon(config, process.argv[1] ?? "", secrets);
1098
- process.stdout.write("Rotated local gateway token and restarted daemon.\n");
1099
- process.stdout.write("No client configs were modified.\n");
1100
- process.stdout.write("Run `mcpx sync <client>` (or `mcpx sync`) to update managed `mcpx` entries with the new token.\n");
1764
+ const auth = program.command("auth").description("Auth utilities");
1765
+ auth.command("set <server>").option("--header <name>", "HTTP header name (default for HTTP servers: Authorization)").option("--env <name>", "Env var name (required for stdio servers)").option("--value <value>", "Auth value/token (if omitted, read from stdin)").option("--secret-name <name>", "Override keychain secret name").option("--raw", "Do not auto-prefix Authorization values with Bearer").description("Store auth in keychain and bind it to an upstream server").action((server, options) => {
1766
+ const config = loadConfig();
1767
+ const spec = getServerSpecOrThrow(config, server);
1768
+ const target = resolveAuthTarget(spec, options.header, options.env);
1769
+ const provided = options.value ?? readSecretValueFromStdin();
1770
+ const authValue = maybePrefixBearer(target, provided, options.raw ?? false);
1771
+ const secretName = options.secretName?.trim() || defaultAuthSecretName(server, target);
1772
+ const secrets = new SecretsManager;
1773
+ secrets.setSecret(secretName, authValue);
1774
+ applyAuthReference(spec, target, toSecretRef(secretName));
1775
+ saveConfig(config);
1776
+ process.stdout.write(`Configured auth for server "${server}" via ${target.kind} "${target.key}".
1777
+ `);
1778
+ process.stdout.write(`Stored secret: ${secretName}
1779
+ `);
1780
+ });
1781
+ auth.command("rm <server>").option("--header <name>", "HTTP header name (default for HTTP servers: Authorization)").option("--env <name>", "Env var name (required for stdio servers)").option("--delete-secret", "Delete referenced keychain secret when removing binding").description("Remove auth binding from an upstream server").action((server, options) => {
1782
+ const config = loadConfig();
1783
+ const spec = getServerSpecOrThrow(config, server);
1784
+ const target = resolveAuthTarget(spec, options.header, options.env);
1785
+ const removedValue = removeAuthReference(spec, target);
1786
+ if (!removedValue) {
1787
+ process.stdout.write(`No auth binding found for server "${server}" at ${target.kind} "${target.key}".
1788
+ `);
1789
+ return;
1790
+ }
1791
+ if (options.deleteSecret) {
1792
+ const secretName = secretRefName(removedValue);
1793
+ if (secretName) {
1794
+ new SecretsManager().removeSecret(secretName);
1795
+ process.stdout.write(`Removed binding and deleted secret: ${secretName}
1796
+ `);
1797
+ } else {
1798
+ process.stdout.write(`Removed binding. Value was inline (no keychain secret deleted).
1799
+ `);
1800
+ }
1801
+ } else {
1802
+ process.stdout.write(`Removed binding.
1803
+ `);
1804
+ }
1805
+ saveConfig(config);
1806
+ });
1807
+ auth.command("show [server]").option("--json", "Output JSON").description("Show configured upstream auth bindings (secret refs only, values redacted)").action((server, options) => {
1808
+ const config = loadConfig();
1809
+ const names = server ? [server] : Object.keys(config.servers);
1810
+ const output = names.map((name) => {
1811
+ const spec = getServerSpecOrThrow(config, name);
1812
+ const bindings = listAuthBindings(spec).map((binding) => ({
1813
+ kind: binding.kind,
1814
+ key: binding.key,
1815
+ value: redactAuthValue(binding.value)
1816
+ }));
1817
+ return {
1818
+ server: name,
1819
+ transport: spec.transport,
1820
+ bindings
1821
+ };
1101
1822
  });
1823
+ if (options.json) {
1824
+ process.stdout.write(`${JSON.stringify({ auth: output }, null, 2)}
1825
+ `);
1826
+ return;
1827
+ }
1828
+ if (output.length === 0) {
1829
+ process.stdout.write(`No upstream servers configured.
1830
+ `);
1831
+ return;
1832
+ }
1833
+ for (const serverEntry of output) {
1834
+ process.stdout.write(`- ${serverEntry.server} (${serverEntry.transport})
1835
+ `);
1836
+ if (serverEntry.bindings.length === 0) {
1837
+ process.stdout.write(` (no auth bindings)
1838
+ `);
1839
+ continue;
1840
+ }
1841
+ for (const binding of serverEntry.bindings) {
1842
+ process.stdout.write(` ${binding.kind}:${binding.key} = ${binding.value}
1843
+ `);
1844
+ }
1845
+ }
1846
+ });
1847
+ auth.command("rotate-local-token").description("Rotate bearer token used for local client->mcpx auth").action(async () => {
1848
+ const secrets = new SecretsManager;
1849
+ secrets.rotateLocalToken("local_gateway_token");
1850
+ const config = loadConfig();
1851
+ await restartDaemon(config, process.argv[1] ?? "", secrets);
1852
+ process.stdout.write(`Rotated local gateway token and restarted daemon.
1853
+ `);
1854
+ process.stdout.write(`No client configs were modified.
1855
+ `);
1856
+ process.stdout.write("Run `mcpx sync <client>` (or `mcpx sync`) to update managed `mcpx` entries with the new token.\n");
1857
+ });
1102
1858
  }
1103
1859
  function registerClientsCommands(program) {
1104
- const clients = program.command("clients").description("Client adapter utilities");
1105
- clients
1106
- .command("list")
1107
- .option("--json", "Output JSON")
1108
- .description("List supported clients and detected config paths")
1109
- .action((options) => {
1110
- const payload = getAdapters().map((adapter) => ({
1111
- id: adapter.id,
1112
- supportsHttp: adapter.supportsHttp(),
1113
- configPath: adapter.detectConfigPath()
1114
- }));
1115
- if (options.json) {
1116
- process.stdout.write(`${JSON.stringify({ clients: payload }, null, 2)}\n`);
1117
- return;
1118
- }
1119
- for (const client of payload) {
1120
- process.stdout.write(`- ${client.id}: ${client.supportsHttp ? "HTTP" : "NO_HTTP"} ${client.configPath ?? "(unknown path)"}\n`);
1121
- }
1122
- });
1860
+ const clients = program.command("clients").description("Client adapter utilities");
1861
+ clients.command("list").option("--json", "Output JSON").description("List supported clients and detected config paths").action((options) => {
1862
+ const payload = getAdapters().map((adapter) => ({
1863
+ id: adapter.id,
1864
+ supportsHttp: adapter.supportsHttp(),
1865
+ configPath: adapter.detectConfigPath()
1866
+ }));
1867
+ if (options.json) {
1868
+ process.stdout.write(`${JSON.stringify({ clients: payload }, null, 2)}
1869
+ `);
1870
+ return;
1871
+ }
1872
+ for (const client of payload) {
1873
+ process.stdout.write(`- ${client.id}: ${client.supportsHttp ? "HTTP" : "NO_HTTP"} ${client.configPath ?? "(unknown path)"}
1874
+ `);
1875
+ }
1876
+ });
1123
1877
  }
1124
1878
  function registerUpdateCommand(program) {
1125
- const update = program.command("update").description("Check for and install updates");
1126
- update
1127
- .command("check")
1128
- .option("--json", "Output JSON")
1129
- .description("Check for available updates")
1130
- .action(async (options) => {
1131
- const status = await checkForUpdates();
1132
- if (options.json) {
1133
- process.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
1134
- return;
1135
- }
1136
- process.stdout.write(`Current: ${status.currentVersion}\n`);
1137
- if (status.latestVersion) {
1138
- process.stdout.write(`Latest: ${status.latestVersion}\n`);
1139
- if (status.updateAvailable) {
1140
- process.stdout.write("Status: Update available. Run `mcpx update` to install.\n");
1141
- }
1142
- else {
1143
- process.stdout.write("Status: Up to date.\n");
1144
- }
1145
- }
1146
- else {
1147
- process.stdout.write(`Status: Unable to check for updates.${status.error ? ` (${status.error})` : ""}\n`);
1148
- }
1149
- });
1150
- update
1151
- .command("install")
1152
- .description("Download and stage the latest update")
1153
- .action(async () => {
1154
- process.stdout.write("Checking for updates...\n");
1155
- const result = await performUpdate();
1156
- process.stdout.write(`${result.message}\n`);
1157
- if (!result.success) {
1158
- process.exitCode = 1;
1159
- }
1160
- });
1161
- update
1162
- .command("rollback")
1163
- .description("Roll back to the previously installed version")
1164
- .action(() => {
1165
- const result = performRollback();
1166
- process.stdout.write(`${result.message}\n`);
1167
- if (!result.success) {
1168
- process.exitCode = 1;
1169
- }
1170
- });
1171
- update
1172
- .command("status")
1173
- .description("Show current update status")
1174
- .action(() => {
1175
- const staged = getStagedUpdate();
1176
- if (staged) {
1177
- process.stdout.write(`Staged version: ${staged.version}\n`);
1178
- process.stdout.write(`Staged at: ${staged.stagedAt}\n`);
1179
- process.stdout.write("Status: Will activate on next run.\n");
1180
- }
1181
- else {
1182
- process.stdout.write("No staged updates.\n");
1183
- }
1184
- });
1879
+ const update = program.command("update").description("Check for and install updates");
1880
+ update.command("check").option("--json", "Output JSON").description("Check for available updates").action(async (options) => {
1881
+ const status = await checkForUpdates();
1882
+ if (options.json) {
1883
+ process.stdout.write(`${JSON.stringify(status, null, 2)}
1884
+ `);
1885
+ return;
1886
+ }
1887
+ process.stdout.write(`Current: ${status.currentVersion}
1888
+ `);
1889
+ if (status.latestVersion) {
1890
+ process.stdout.write(`Latest: ${status.latestVersion}
1891
+ `);
1892
+ if (status.updateAvailable) {
1893
+ process.stdout.write("Status: Update available. Run `mcpx update` to install.\n");
1894
+ } else {
1895
+ process.stdout.write(`Status: Up to date.
1896
+ `);
1897
+ }
1898
+ } else {
1899
+ process.stdout.write(`Status: Unable to check for updates.${status.error ? ` (${status.error})` : ""}
1900
+ `);
1901
+ }
1902
+ });
1903
+ update.command("install").description("Download and stage the latest update").action(async () => {
1904
+ process.stdout.write(`Checking for updates...
1905
+ `);
1906
+ const result = await performUpdate();
1907
+ process.stdout.write(`${result.message}
1908
+ `);
1909
+ if (!result.success) {
1910
+ process.exitCode = 1;
1911
+ }
1912
+ });
1913
+ update.command("rollback").description("Roll back to the previously installed version").action(() => {
1914
+ const result = performRollback();
1915
+ process.stdout.write(`${result.message}
1916
+ `);
1917
+ if (!result.success) {
1918
+ process.exitCode = 1;
1919
+ }
1920
+ });
1921
+ update.command("status").description("Show current update status").action(() => {
1922
+ const staged = getStagedUpdate();
1923
+ if (staged) {
1924
+ process.stdout.write(`Staged version: ${staged.version}
1925
+ `);
1926
+ process.stdout.write(`Staged at: ${staged.stagedAt}
1927
+ `);
1928
+ process.stdout.write(`Status: Will activate on next run.
1929
+ `);
1930
+ } else {
1931
+ process.stdout.write(`No staged updates.
1932
+ `);
1933
+ }
1934
+ });
1185
1935
  }
1186
1936
  function registerMcpCompat(program) {
1187
- const mcp = program.command("mcp").description("Compatibility namespace for MCP commands");
1188
- registerAddCommand(mcp);
1189
- registerRemoveCommand(mcp);
1190
- registerEnabledCommand(mcp, true);
1191
- registerEnabledCommand(mcp, false);
1192
- registerListCommand(mcp);
1193
- }
1194
- export async function runCli(argv = process.argv) {
1195
- if (process.env.MCPX_UPDATE_CHILD === "1") {
1196
- await runBackgroundUpdate();
1197
- return;
1198
- }
1199
- const rawArgs = argv.slice(2);
1200
- const isUpdateCommand = rawArgs[0] === "update";
1201
- if (!isUpdateCommand) {
1202
- const stagedCliPath = getStagedCliPath();
1203
- const stagedInfo = getStagedUpdate();
1204
- if (stagedCliPath && stagedInfo && stagedCliPath !== argv[1]) {
1205
- execFileSync(process.execPath, [stagedCliPath, ...rawArgs], {
1206
- stdio: "inherit",
1207
- env: {
1208
- ...process.env,
1209
- MCPX_USING_STAGED: "1"
1210
- }
1211
- });
1212
- return;
1213
- }
1214
- }
1215
- // Check for client-native compatibility patterns before Commander parses
1216
- if (rawArgs.length > 0) {
1217
- const compat = parseCompatibilityArgs(rawArgs);
1218
- // If unsupported client detected, show error and exit
1219
- if (compat.error && compat.client !== null) {
1220
- process.stderr.write(`Error: ${compat.error}\n`);
1221
- process.exit(1);
1222
- }
1223
- // If valid compatibility command, normalize and inject into add flow
1224
- if (compat.normalizedArgs !== null) {
1225
- // Replace argv with normalized args for the add command
1226
- // mcpx add <normalizedArgs...>
1227
- argv = [argv[0], argv[1], "add", ...compat.normalizedArgs];
1228
- }
1229
- }
1230
- const program = new Command();
1231
- program.name("mcpx").description("HTTP-first MCP gateway and multi-client installer").version(APP_VERSION);
1232
- registerAddCommand(program);
1233
- registerRemoveCommand(program);
1234
- registerEnabledCommand(program, true);
1235
- registerEnabledCommand(program, false);
1236
- registerListCommand(program);
1237
- registerSyncCommand(program);
1238
- registerStatusCommand(program);
1239
- registerDoctorCommand(program);
1240
- registerDaemonCommands(program);
1241
- registerSecretsCommands(program);
1242
- registerAuthCommands(program);
1243
- registerClientsCommands(program);
1244
- registerMcpCompat(program);
1245
- registerUpdateCommand(program);
1246
- await program.parseAsync(argv);
1937
+ const mcp = program.command("mcp").description("Compatibility namespace for MCP commands");
1938
+ registerAddCommand(mcp);
1939
+ registerRemoveCommand(mcp);
1940
+ registerEnabledCommand(mcp, true);
1941
+ registerEnabledCommand(mcp, false);
1942
+ registerListCommand(mcp);
1943
+ }
1944
+ async function runCli(argv = process.argv) {
1945
+ if (process.env.MCPX_UPDATE_CHILD === "1") {
1946
+ await runBackgroundUpdate();
1947
+ return;
1948
+ }
1949
+ const rawArgs = argv.slice(2);
1950
+ const isUpdateCommand = rawArgs[0] === "update";
1951
+ if (!isUpdateCommand) {
1952
+ const stagedCliPath = getStagedCliPath();
1953
+ const stagedInfo = getStagedUpdate();
1954
+ if (stagedCliPath && stagedInfo && stagedCliPath !== argv[1]) {
1955
+ execFileSync(process.execPath, [stagedCliPath, ...rawArgs], {
1956
+ stdio: "inherit",
1957
+ env: {
1958
+ ...process.env,
1959
+ MCPX_USING_STAGED: "1"
1960
+ }
1961
+ });
1962
+ return;
1963
+ }
1964
+ }
1965
+ if (rawArgs.length > 0) {
1966
+ const compat = parseCompatibilityArgs(rawArgs);
1967
+ if (compat.error && compat.client !== null) {
1968
+ process.stderr.write(`Error: ${compat.error}
1969
+ `);
1970
+ process.exit(1);
1971
+ }
1972
+ if (compat.normalizedArgs !== null) {
1973
+ argv = [argv[0], argv[1], "add", ...compat.normalizedArgs];
1974
+ }
1975
+ }
1976
+ const program = new Command;
1977
+ program.name("mcpx").description("HTTP-first MCP gateway and multi-client installer").version(APP_VERSION);
1978
+ registerAddCommand(program);
1979
+ registerRemoveCommand(program);
1980
+ registerEnabledCommand(program, true);
1981
+ registerEnabledCommand(program, false);
1982
+ registerListCommand(program);
1983
+ registerSyncCommand(program);
1984
+ registerStatusCommand(program);
1985
+ registerDoctorCommand(program);
1986
+ registerDaemonCommands(program);
1987
+ registerSecretsCommands(program);
1988
+ registerAuthCommands(program);
1989
+ registerClientsCommands(program);
1990
+ registerMcpCompat(program);
1991
+ registerUpdateCommand(program);
1992
+ await program.parseAsync(argv);
1247
1993
  }
1248
1994
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
1249
- void runCli().catch((error) => {
1250
- process.stderr.write(`${error.message}\n`);
1251
- process.exit(1);
1252
- });
1995
+ runCli().catch((error) => {
1996
+ process.stderr.write(`${error.message}
1997
+ `);
1998
+ process.exit(1);
1999
+ });
1253
2000
  }
1254
- //# sourceMappingURL=cli.js.map
2001
+ export {
2002
+ runCli
2003
+ };