@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.
- package/dist/adapters/claude-desktop.d.ts +1 -0
- package/dist/adapters/claude-desktop.d.ts.map +1 -0
- package/dist/adapters/claude.d.ts +1 -0
- package/dist/adapters/claude.d.ts.map +1 -0
- package/dist/adapters/cline.d.ts +1 -0
- package/dist/adapters/cline.d.ts.map +1 -0
- package/dist/adapters/codex.d.ts +1 -0
- package/dist/adapters/codex.d.ts.map +1 -0
- package/dist/adapters/cursor.d.ts +1 -0
- package/dist/adapters/cursor.d.ts.map +1 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/kiro.d.ts +1 -0
- package/dist/adapters/kiro.d.ts.map +1 -0
- package/dist/adapters/opencode.d.ts +1 -0
- package/dist/adapters/opencode.d.ts.map +1 -0
- package/dist/adapters/qwen.d.ts +1 -0
- package/dist/adapters/qwen.d.ts.map +1 -0
- package/dist/adapters/utils/index.d.ts +2 -0
- package/dist/adapters/utils/index.d.ts.map +1 -0
- package/dist/adapters/vscode.d.ts +1 -0
- package/dist/adapters/vscode.d.ts.map +1 -0
- package/dist/cli-7rggq938.js +3292 -0
- package/dist/cli-ezvj44tr.js +3292 -0
- package/dist/cli-q8de7zb0.js +12 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1880 -1131
- package/dist/compat/claude.d.ts +1 -0
- package/dist/compat/claude.d.ts.map +1 -0
- package/dist/compat/codex.d.ts +1 -0
- package/dist/compat/codex.d.ts.map +1 -0
- package/dist/compat/index.d.ts +1 -0
- package/dist/compat/index.d.ts.map +1 -0
- package/dist/compat/qwen.d.ts +1 -0
- package/dist/compat/qwen.d.ts.map +1 -0
- package/dist/compat/unsupported.d.ts +1 -0
- package/dist/compat/unsupported.d.ts.map +1 -0
- package/dist/compat/vscode.d.ts +1 -0
- package/dist/compat/vscode.d.ts.map +1 -0
- package/dist/core/auth-probe.d.ts +1 -0
- package/dist/core/auth-probe.d.ts.map +1 -0
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/daemon.d.ts +1 -0
- package/dist/core/daemon.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +81 -24
- package/dist/core/managed-index.d.ts +1 -0
- package/dist/core/managed-index.d.ts.map +1 -0
- package/dist/core/paths.d.ts +2 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/registry.d.ts +1 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/secrets.d.ts +1 -0
- package/dist/core/secrets.d.ts.map +1 -0
- package/dist/core/server-auth.d.ts +1 -0
- package/dist/core/server-auth.d.ts.map +1 -0
- package/dist/core/status.d.ts +1 -0
- package/dist/core/status.d.ts.map +1 -0
- package/dist/core/sync.d.ts +1 -0
- package/dist/core/sync.d.ts.map +1 -0
- package/dist/core/update-manager.d.ts +1 -0
- package/dist/core/update-manager.d.ts.map +1 -0
- package/dist/core/update.d.ts +1 -0
- package/dist/core/update.d.ts.map +1 -0
- package/dist/gateway/server.d.ts +1 -0
- package/dist/gateway/server.d.ts.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -10
- package/dist/util/fs.d.ts +1 -0
- package/dist/util/fs.d.ts.map +1 -0
- package/dist/version.d.ts +2 -1
- package/dist/version.d.ts.map +1 -0
- package/package.json +10 -11
- package/dist/adapters/claude-desktop.js +0 -115
- package/dist/adapters/claude-desktop.js.map +0 -1
- package/dist/adapters/claude.js +0 -131
- package/dist/adapters/claude.js.map +0 -1
- package/dist/adapters/cline.js +0 -135
- package/dist/adapters/cline.js.map +0 -1
- package/dist/adapters/codex.js +0 -125
- package/dist/adapters/codex.js.map +0 -1
- package/dist/adapters/cursor.js +0 -122
- package/dist/adapters/cursor.js.map +0 -1
- package/dist/adapters/index.js +0 -23
- package/dist/adapters/index.js.map +0 -1
- package/dist/adapters/kiro.js +0 -120
- package/dist/adapters/kiro.js.map +0 -1
- package/dist/adapters/opencode.js +0 -122
- package/dist/adapters/opencode.js.map +0 -1
- package/dist/adapters/qwen.js +0 -113
- package/dist/adapters/qwen.js.map +0 -1
- package/dist/adapters/utils/index.js +0 -134
- package/dist/adapters/utils/index.js.map +0 -1
- package/dist/adapters/vscode.js +0 -122
- package/dist/adapters/vscode.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/compat/claude.js +0 -252
- package/dist/compat/claude.js.map +0 -1
- package/dist/compat/codex.js +0 -179
- package/dist/compat/codex.js.map +0 -1
- package/dist/compat/index.js +0 -108
- package/dist/compat/index.js.map +0 -1
- package/dist/compat/qwen.js +0 -316
- package/dist/compat/qwen.js.map +0 -1
- package/dist/compat/unsupported.js +0 -47
- package/dist/compat/unsupported.js.map +0 -1
- package/dist/compat/vscode.js +0 -164
- package/dist/compat/vscode.js.map +0 -1
- package/dist/core/auth-probe.js +0 -54
- package/dist/core/auth-probe.js.map +0 -1
- package/dist/core/config.js +0 -76
- package/dist/core/config.js.map +0 -1
- package/dist/core/daemon.js +0 -176
- package/dist/core/daemon.js.map +0 -1
- package/dist/core/index.js.map +0 -1
- package/dist/core/managed-index.js +0 -22
- package/dist/core/managed-index.js.map +0 -1
- package/dist/core/paths.js +0 -55
- package/dist/core/paths.js.map +0 -1
- package/dist/core/registry.js +0 -51
- package/dist/core/registry.js.map +0 -1
- package/dist/core/secrets.js +0 -108
- package/dist/core/secrets.js.map +0 -1
- package/dist/core/server-auth.js +0 -112
- package/dist/core/server-auth.js.map +0 -1
- package/dist/core/status.js +0 -65
- package/dist/core/status.js.map +0 -1
- package/dist/core/sync.js +0 -128
- package/dist/core/sync.js.map +0 -1
- package/dist/core/update-manager.js +0 -170
- package/dist/core/update-manager.js.map +0 -1
- package/dist/core/update.js +0 -111
- package/dist/core/update.js.map +0 -1
- package/dist/gateway/server.js +0 -995
- package/dist/gateway/server.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/util/fs.js +0 -34
- package/dist/util/fs.js.map +0 -1
- package/dist/version.js +0 -2
- package/dist/version.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,1254 +1,2003 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
815
|
+
return parseKeyValueFlag(header, "header");
|
|
37
816
|
}
|
|
38
817
|
function parseEnvVar(envVar) {
|
|
39
|
-
|
|
818
|
+
return parseKeyValueFlag(envVar, "env");
|
|
40
819
|
}
|
|
41
820
|
function isHttpUrl(value) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
891
|
+
if (value.startsWith("secret://")) {
|
|
892
|
+
return value;
|
|
893
|
+
}
|
|
894
|
+
return "<inline>";
|
|
119
895
|
}
|
|
120
896
|
function parseClientList(values) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
910
|
+
if (hasErrors) {
|
|
911
|
+
process.exitCode = 2;
|
|
912
|
+
}
|
|
137
913
|
}
|
|
138
914
|
function printSyncSummary(summary, asJson = false) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
980
|
+
const normalized = value.trim().toLowerCase();
|
|
981
|
+
return normalized === "n" || normalized === "no";
|
|
201
982
|
}
|
|
202
983
|
async function maybeAutoConfigureAuthForAddedServer(serverName, spec, secrets) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
274
|
-
|
|
1060
|
+
const config = loadConfig();
|
|
1061
|
+
return buildStatusReport(config, loadManagedIndex(), getDaemonStatus(config));
|
|
275
1062
|
}
|
|
276
1063
|
function daemonEmoji(running) {
|
|
277
|
-
|
|
1064
|
+
return running ? "✅" : "⚠️";
|
|
278
1065
|
}
|
|
279
1066
|
function clientStatusEmoji(status) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
1095
|
+
if (status.running) {
|
|
1096
|
+
return `running (pid ${status.pid})`;
|
|
1097
|
+
}
|
|
1098
|
+
return "stopped";
|
|
312
1099
|
}
|
|
313
1100
|
function formatAuthSummary(authBindings) {
|
|
314
|
-
|
|
1101
|
+
return authBindings.map((binding) => `${binding.kind}:${binding.key}`).join(", ");
|
|
315
1102
|
}
|
|
316
1103
|
function listSyncedClientLabels(server) {
|
|
317
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
1159
|
+
return (await rl.question(prompt)).trim();
|
|
363
1160
|
}
|
|
364
1161
|
async function promptLineWithDefault(rl, prompt, defaultValue) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
1174
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
378
1175
|
}
|
|
379
1176
|
function renderMenuScreen(titleLines, options, selectedIndex, cancelHint) {
|
|
380
|
-
|
|
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
|
-
|
|
1205
|
+
process.stdout.write(`${line}
|
|
1206
|
+
`);
|
|
383
1207
|
}
|
|
384
|
-
process.stdout.write(
|
|
1208
|
+
process.stdout.write(`
|
|
1209
|
+
`);
|
|
385
1210
|
options.forEach((option, index) => {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
1440
|
+
}
|
|
1441
|
+
return lines;
|
|
630
1442
|
}
|
|
631
1443
|
function buildServerMenuDetail(server) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1666
|
+
check: `client_${adapter.id}`,
|
|
1667
|
+
status: "warn",
|
|
1668
|
+
details: "no known config path"
|
|
877
1669
|
});
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
.
|
|
1054
|
-
|
|
1055
|
-
|
|
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
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1995
|
+
runCli().catch((error) => {
|
|
1996
|
+
process.stderr.write(`${error.message}
|
|
1997
|
+
`);
|
|
1998
|
+
process.exit(1);
|
|
1999
|
+
});
|
|
1253
2000
|
}
|
|
1254
|
-
|
|
2001
|
+
export {
|
|
2002
|
+
runCli
|
|
2003
|
+
};
|