mcpman 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/dist/chunk-QY22QTBR.js +229 -0
- package/dist/client-detector-SUIJSIYM.js +11 -0
- package/dist/index.cjs +1407 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1059 -0
- package/package.json +40 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1407 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
|
|
33
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
34
|
+
var init_cjs_shims = __esm({
|
|
35
|
+
"node_modules/tsup/assets/cjs_shims.js"() {
|
|
36
|
+
"use strict";
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// src/clients/types.ts
|
|
41
|
+
var ConfigParseError, ConfigWriteError;
|
|
42
|
+
var init_types = __esm({
|
|
43
|
+
"src/clients/types.ts"() {
|
|
44
|
+
"use strict";
|
|
45
|
+
init_cjs_shims();
|
|
46
|
+
ConfigParseError = class extends Error {
|
|
47
|
+
constructor(configPath, cause) {
|
|
48
|
+
super(`Failed to parse config: ${configPath} \u2014 ${String(cause)}`);
|
|
49
|
+
this.configPath = configPath;
|
|
50
|
+
this.name = "ConfigParseError";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
ConfigWriteError = class extends Error {
|
|
54
|
+
constructor(configPath, cause) {
|
|
55
|
+
super(`Failed to write config: ${configPath} \u2014 ${String(cause)}`);
|
|
56
|
+
this.configPath = configPath;
|
|
57
|
+
this.name = "ConfigWriteError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// src/clients/base-client-handler.ts
|
|
64
|
+
async function atomicWrite(filePath, content) {
|
|
65
|
+
const tmpPath = `${filePath}.tmp`;
|
|
66
|
+
try {
|
|
67
|
+
await import_node_fs.default.promises.mkdir(import_node_path.default.dirname(filePath), { recursive: true });
|
|
68
|
+
await import_node_fs.default.promises.writeFile(tmpPath, content, { encoding: "utf-8", mode: 384 });
|
|
69
|
+
await import_node_fs.default.promises.rename(tmpPath, filePath);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
try {
|
|
72
|
+
await import_node_fs.default.promises.unlink(tmpPath);
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function pathExists(p5) {
|
|
79
|
+
try {
|
|
80
|
+
await import_node_fs.default.promises.access(p5);
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
var import_node_fs, import_node_path, BaseClientHandler;
|
|
87
|
+
var init_base_client_handler = __esm({
|
|
88
|
+
"src/clients/base-client-handler.ts"() {
|
|
89
|
+
"use strict";
|
|
90
|
+
init_cjs_shims();
|
|
91
|
+
import_node_fs = __toESM(require("fs"), 1);
|
|
92
|
+
import_node_path = __toESM(require("path"), 1);
|
|
93
|
+
init_types();
|
|
94
|
+
BaseClientHandler = class {
|
|
95
|
+
async isInstalled() {
|
|
96
|
+
const dir = import_node_path.default.dirname(this.getConfigPath());
|
|
97
|
+
return pathExists(dir);
|
|
98
|
+
}
|
|
99
|
+
/** Read raw JSON from disk, return empty object if file missing */
|
|
100
|
+
async readRaw() {
|
|
101
|
+
const configPath = this.getConfigPath();
|
|
102
|
+
try {
|
|
103
|
+
const raw = await import_node_fs.default.promises.readFile(configPath, "utf-8");
|
|
104
|
+
return JSON.parse(raw);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err.code === "ENOENT") {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
throw new ConfigParseError(configPath, err);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Serialize raw object to disk atomically */
|
|
113
|
+
async writeRaw(data) {
|
|
114
|
+
const configPath = this.getConfigPath();
|
|
115
|
+
try {
|
|
116
|
+
await atomicWrite(configPath, JSON.stringify(data, null, 2));
|
|
117
|
+
} catch (err) {
|
|
118
|
+
throw new ConfigWriteError(configPath, err);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Convert raw JSON to ClientConfig — override for non-standard formats */
|
|
122
|
+
toClientConfig(raw) {
|
|
123
|
+
const mcpServers = raw.mcpServers ?? {};
|
|
124
|
+
return { servers: mcpServers };
|
|
125
|
+
}
|
|
126
|
+
/** Merge ClientConfig back into raw JSON — override for non-standard formats */
|
|
127
|
+
fromClientConfig(raw, config) {
|
|
128
|
+
return { ...raw, mcpServers: config.servers };
|
|
129
|
+
}
|
|
130
|
+
async readConfig() {
|
|
131
|
+
const raw = await this.readRaw();
|
|
132
|
+
return this.toClientConfig(raw);
|
|
133
|
+
}
|
|
134
|
+
async writeConfig(config) {
|
|
135
|
+
const raw = await this.readRaw();
|
|
136
|
+
await this.writeRaw(this.fromClientConfig(raw, config));
|
|
137
|
+
}
|
|
138
|
+
async addServer(name, entry) {
|
|
139
|
+
const config = await this.readConfig();
|
|
140
|
+
config.servers[name] = entry;
|
|
141
|
+
await this.writeConfig(config);
|
|
142
|
+
}
|
|
143
|
+
async removeServer(name) {
|
|
144
|
+
const config = await this.readConfig();
|
|
145
|
+
delete config.servers[name];
|
|
146
|
+
await this.writeConfig(config);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// src/utils/paths.ts
|
|
153
|
+
function getHomedir() {
|
|
154
|
+
return import_node_os.default.homedir();
|
|
155
|
+
}
|
|
156
|
+
function getAppDataDir() {
|
|
157
|
+
const home = getHomedir();
|
|
158
|
+
if (process.platform === "darwin") {
|
|
159
|
+
return import_node_path2.default.join(home, "Library", "Application Support");
|
|
160
|
+
}
|
|
161
|
+
if (process.platform === "win32") {
|
|
162
|
+
return process.env.APPDATA ?? import_node_path2.default.join(home, "AppData", "Roaming");
|
|
163
|
+
}
|
|
164
|
+
return process.env.XDG_CONFIG_HOME ?? import_node_path2.default.join(home, ".config");
|
|
165
|
+
}
|
|
166
|
+
function resolveConfigPath(client) {
|
|
167
|
+
const appData = getAppDataDir();
|
|
168
|
+
const home = getHomedir();
|
|
169
|
+
switch (client) {
|
|
170
|
+
case "claude-desktop":
|
|
171
|
+
return import_node_path2.default.join(appData, "Claude", "claude_desktop_config.json");
|
|
172
|
+
case "cursor":
|
|
173
|
+
return import_node_path2.default.join(
|
|
174
|
+
appData,
|
|
175
|
+
"Cursor",
|
|
176
|
+
"User",
|
|
177
|
+
"globalStorage",
|
|
178
|
+
"cursor.mcp",
|
|
179
|
+
"mcp.json"
|
|
180
|
+
);
|
|
181
|
+
case "windsurf":
|
|
182
|
+
return import_node_path2.default.join(
|
|
183
|
+
appData,
|
|
184
|
+
"Windsurf",
|
|
185
|
+
"User",
|
|
186
|
+
"globalStorage",
|
|
187
|
+
"windsurf.mcpConfigJson",
|
|
188
|
+
"mcp.json"
|
|
189
|
+
);
|
|
190
|
+
case "vscode":
|
|
191
|
+
if (process.platform === "darwin") {
|
|
192
|
+
return import_node_path2.default.join(appData, "Code", "User", "settings.json");
|
|
193
|
+
}
|
|
194
|
+
if (process.platform === "win32") {
|
|
195
|
+
return import_node_path2.default.join(appData, "Code", "User", "settings.json");
|
|
196
|
+
}
|
|
197
|
+
return import_node_path2.default.join(home, ".config", "Code", "User", "settings.json");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
var import_node_os, import_node_path2;
|
|
201
|
+
var init_paths = __esm({
|
|
202
|
+
"src/utils/paths.ts"() {
|
|
203
|
+
"use strict";
|
|
204
|
+
init_cjs_shims();
|
|
205
|
+
import_node_os = __toESM(require("os"), 1);
|
|
206
|
+
import_node_path2 = __toESM(require("path"), 1);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// src/clients/claude-desktop.ts
|
|
211
|
+
var ClaudeDesktopHandler;
|
|
212
|
+
var init_claude_desktop = __esm({
|
|
213
|
+
"src/clients/claude-desktop.ts"() {
|
|
214
|
+
"use strict";
|
|
215
|
+
init_cjs_shims();
|
|
216
|
+
init_base_client_handler();
|
|
217
|
+
init_paths();
|
|
218
|
+
ClaudeDesktopHandler = class extends BaseClientHandler {
|
|
219
|
+
type = "claude-desktop";
|
|
220
|
+
displayName = "Claude Desktop";
|
|
221
|
+
getConfigPath() {
|
|
222
|
+
return resolveConfigPath("claude-desktop");
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// src/clients/cursor.ts
|
|
229
|
+
var CursorHandler;
|
|
230
|
+
var init_cursor = __esm({
|
|
231
|
+
"src/clients/cursor.ts"() {
|
|
232
|
+
"use strict";
|
|
233
|
+
init_cjs_shims();
|
|
234
|
+
init_base_client_handler();
|
|
235
|
+
init_paths();
|
|
236
|
+
CursorHandler = class extends BaseClientHandler {
|
|
237
|
+
type = "cursor";
|
|
238
|
+
displayName = "Cursor";
|
|
239
|
+
getConfigPath() {
|
|
240
|
+
return resolveConfigPath("cursor");
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// src/clients/vscode.ts
|
|
247
|
+
var VSCodeHandler;
|
|
248
|
+
var init_vscode = __esm({
|
|
249
|
+
"src/clients/vscode.ts"() {
|
|
250
|
+
"use strict";
|
|
251
|
+
init_cjs_shims();
|
|
252
|
+
init_base_client_handler();
|
|
253
|
+
init_paths();
|
|
254
|
+
VSCodeHandler = class extends BaseClientHandler {
|
|
255
|
+
type = "vscode";
|
|
256
|
+
displayName = "VS Code";
|
|
257
|
+
getConfigPath() {
|
|
258
|
+
return resolveConfigPath("vscode");
|
|
259
|
+
}
|
|
260
|
+
toClientConfig(raw) {
|
|
261
|
+
const mcp = raw.mcp ?? {};
|
|
262
|
+
const servers = mcp.servers ?? {};
|
|
263
|
+
return { servers };
|
|
264
|
+
}
|
|
265
|
+
fromClientConfig(raw, config) {
|
|
266
|
+
const existingMcp = raw.mcp ?? {};
|
|
267
|
+
return {
|
|
268
|
+
...raw,
|
|
269
|
+
mcp: { ...existingMcp, servers: config.servers }
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// src/clients/windsurf.ts
|
|
277
|
+
var WindsurfHandler;
|
|
278
|
+
var init_windsurf = __esm({
|
|
279
|
+
"src/clients/windsurf.ts"() {
|
|
280
|
+
"use strict";
|
|
281
|
+
init_cjs_shims();
|
|
282
|
+
init_base_client_handler();
|
|
283
|
+
init_paths();
|
|
284
|
+
WindsurfHandler = class extends BaseClientHandler {
|
|
285
|
+
type = "windsurf";
|
|
286
|
+
displayName = "Windsurf";
|
|
287
|
+
getConfigPath() {
|
|
288
|
+
return resolveConfigPath("windsurf");
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// src/clients/client-detector.ts
|
|
295
|
+
var client_detector_exports = {};
|
|
296
|
+
__export(client_detector_exports, {
|
|
297
|
+
getAllClientTypes: () => getAllClientTypes,
|
|
298
|
+
getClient: () => getClient,
|
|
299
|
+
getInstalledClients: () => getInstalledClients
|
|
300
|
+
});
|
|
301
|
+
function getAllClientTypes() {
|
|
302
|
+
return ["claude-desktop", "cursor", "vscode", "windsurf"];
|
|
303
|
+
}
|
|
304
|
+
function getClient(type) {
|
|
305
|
+
switch (type) {
|
|
306
|
+
case "claude-desktop":
|
|
307
|
+
return new ClaudeDesktopHandler();
|
|
308
|
+
case "cursor":
|
|
309
|
+
return new CursorHandler();
|
|
310
|
+
case "vscode":
|
|
311
|
+
return new VSCodeHandler();
|
|
312
|
+
case "windsurf":
|
|
313
|
+
return new WindsurfHandler();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function getInstalledClients() {
|
|
317
|
+
const all = getAllClientTypes().map(getClient);
|
|
318
|
+
const results = await Promise.all(
|
|
319
|
+
all.map(async (handler) => ({ handler, installed: await handler.isInstalled() }))
|
|
320
|
+
);
|
|
321
|
+
return results.filter((r) => r.installed).map((r) => r.handler);
|
|
322
|
+
}
|
|
323
|
+
var init_client_detector = __esm({
|
|
324
|
+
"src/clients/client-detector.ts"() {
|
|
325
|
+
"use strict";
|
|
326
|
+
init_cjs_shims();
|
|
327
|
+
init_claude_desktop();
|
|
328
|
+
init_cursor();
|
|
329
|
+
init_vscode();
|
|
330
|
+
init_windsurf();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// src/index.ts
|
|
335
|
+
init_cjs_shims();
|
|
336
|
+
var import_citty6 = require("citty");
|
|
337
|
+
|
|
338
|
+
// src/commands/doctor.ts
|
|
339
|
+
init_cjs_shims();
|
|
340
|
+
var import_citty = require("citty");
|
|
341
|
+
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
342
|
+
|
|
343
|
+
// src/core/server-inventory.ts
|
|
344
|
+
init_cjs_shims();
|
|
345
|
+
init_client_detector();
|
|
346
|
+
async function getInstalledServers(clientFilter) {
|
|
347
|
+
const clients = await getInstalledClients();
|
|
348
|
+
const filtered = clientFilter ? clients.filter((c) => c.type === clientFilter) : clients;
|
|
349
|
+
const serverMap = /* @__PURE__ */ new Map();
|
|
350
|
+
for (const client of filtered) {
|
|
351
|
+
let config;
|
|
352
|
+
try {
|
|
353
|
+
config = await client.readConfig();
|
|
354
|
+
} catch {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
for (const [name, entry] of Object.entries(config.servers)) {
|
|
358
|
+
const existing = serverMap.get(name);
|
|
359
|
+
if (existing) {
|
|
360
|
+
if (!existing.clients.includes(client.type)) {
|
|
361
|
+
existing.clients.push(client.type);
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
serverMap.set(name, {
|
|
365
|
+
name,
|
|
366
|
+
clients: [client.type],
|
|
367
|
+
config: entry,
|
|
368
|
+
status: "unknown"
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return Array.from(serverMap.values());
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/core/health-checker.ts
|
|
377
|
+
init_cjs_shims();
|
|
378
|
+
|
|
379
|
+
// src/core/diagnostics.ts
|
|
380
|
+
init_cjs_shims();
|
|
381
|
+
var import_node_util = require("util");
|
|
382
|
+
var import_node_child_process2 = require("child_process");
|
|
383
|
+
|
|
384
|
+
// src/core/mcp-process-checks.ts
|
|
385
|
+
init_cjs_shims();
|
|
386
|
+
var import_node_child_process = require("child_process");
|
|
387
|
+
async function checkProcessSpawn(command, args, env, timeoutMs = 3e3) {
|
|
388
|
+
return new Promise((resolve) => {
|
|
389
|
+
let settled = false;
|
|
390
|
+
const child = (0, import_node_child_process.spawn)(command, args, {
|
|
391
|
+
env: { ...process.env, ...env },
|
|
392
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
393
|
+
});
|
|
394
|
+
const timer = setTimeout(() => {
|
|
395
|
+
if (!settled) {
|
|
396
|
+
settled = true;
|
|
397
|
+
child.kill("SIGTERM");
|
|
398
|
+
resolve({ name: "Process", passed: true, message: "starts successfully (still running)" });
|
|
399
|
+
}
|
|
400
|
+
}, timeoutMs);
|
|
401
|
+
child.on("error", (err) => {
|
|
402
|
+
if (!settled) {
|
|
403
|
+
settled = true;
|
|
404
|
+
clearTimeout(timer);
|
|
405
|
+
resolve({ name: "Process", passed: false, message: `spawn error: ${err.message}` });
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
child.on("exit", (code) => {
|
|
409
|
+
if (!settled) {
|
|
410
|
+
settled = true;
|
|
411
|
+
clearTimeout(timer);
|
|
412
|
+
if (code === 0 || code === null) {
|
|
413
|
+
resolve({ name: "Process", passed: true, message: "exits cleanly" });
|
|
414
|
+
} else {
|
|
415
|
+
resolve({ name: "Process", passed: false, message: `exits with code ${code}` });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
var MCP_INIT_REQUEST = JSON.stringify({
|
|
422
|
+
jsonrpc: "2.0",
|
|
423
|
+
id: 1,
|
|
424
|
+
method: "initialize",
|
|
425
|
+
params: {
|
|
426
|
+
protocolVersion: "2024-11-05",
|
|
427
|
+
capabilities: {},
|
|
428
|
+
clientInfo: { name: "mcpman-doctor", version: "0.1.0" }
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
async function checkMcpHandshake(command, args, env, timeoutMs = 5e3) {
|
|
432
|
+
return new Promise((resolve) => {
|
|
433
|
+
let settled = false;
|
|
434
|
+
const start = Date.now();
|
|
435
|
+
let stdout = "";
|
|
436
|
+
const child = (0, import_node_child_process.spawn)(command, args, {
|
|
437
|
+
env: { ...process.env, ...env },
|
|
438
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
439
|
+
});
|
|
440
|
+
const done = (result) => {
|
|
441
|
+
if (!settled) {
|
|
442
|
+
settled = true;
|
|
443
|
+
try {
|
|
444
|
+
child.kill("SIGTERM");
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
resolve(result);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
const timer = setTimeout(() => {
|
|
451
|
+
done({ name: "MCP handshake", passed: false, message: "no response (timeout 5s)" });
|
|
452
|
+
}, timeoutMs);
|
|
453
|
+
child.on("error", (err) => {
|
|
454
|
+
clearTimeout(timer);
|
|
455
|
+
done({ name: "MCP handshake", passed: false, message: `spawn error: ${err.message}` });
|
|
456
|
+
});
|
|
457
|
+
child.stdout?.on("data", (chunk) => {
|
|
458
|
+
stdout += chunk.toString();
|
|
459
|
+
for (const line of stdout.split("\n")) {
|
|
460
|
+
const trimmed = line.trim();
|
|
461
|
+
if (!trimmed) continue;
|
|
462
|
+
try {
|
|
463
|
+
const parsed = JSON.parse(trimmed);
|
|
464
|
+
if (parsed.jsonrpc === "2.0" && parsed.id === 1) {
|
|
465
|
+
clearTimeout(timer);
|
|
466
|
+
done({ name: "MCP handshake", passed: true, message: `responds in ${Date.now() - start}ms` });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
child.on("exit", (code) => {
|
|
474
|
+
clearTimeout(timer);
|
|
475
|
+
if (!settled) {
|
|
476
|
+
done({ name: "MCP handshake", passed: false, message: `process exited with code ${code} before responding` });
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
child.stdin?.write(MCP_INIT_REQUEST + "\n");
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/core/diagnostics.ts
|
|
484
|
+
var execAsync = (0, import_node_util.promisify)(import_node_child_process2.exec);
|
|
485
|
+
async function checkRuntime(command) {
|
|
486
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
487
|
+
const runtimeCmd = command === "npx" ? "node" : command === "uvx" ? "python3" : command;
|
|
488
|
+
try {
|
|
489
|
+
const { stdout } = await execAsync(`${cmd} ${runtimeCmd}`);
|
|
490
|
+
const version = await getRuntimeVersion(runtimeCmd);
|
|
491
|
+
return {
|
|
492
|
+
name: "Runtime",
|
|
493
|
+
passed: true,
|
|
494
|
+
message: `${runtimeCmd}${version ? ` ${version}` : ""} (${stdout.trim()})`
|
|
495
|
+
};
|
|
496
|
+
} catch {
|
|
497
|
+
return {
|
|
498
|
+
name: "Runtime",
|
|
499
|
+
passed: false,
|
|
500
|
+
message: `${runtimeCmd} not found on PATH`,
|
|
501
|
+
fix: getInstallFix(runtimeCmd)
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function getRuntimeVersion(cmd) {
|
|
506
|
+
try {
|
|
507
|
+
const flag = cmd === "docker" ? "version --format '{{.Client.Version}}'" : "--version";
|
|
508
|
+
const { stdout } = await execAsync(`${cmd} ${flag}`);
|
|
509
|
+
const match = stdout.match(/\d+\.\d+[\.\d]*/);
|
|
510
|
+
return match ? `v${match[0]}` : "";
|
|
511
|
+
} catch {
|
|
512
|
+
return "";
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function getInstallFix(cmd) {
|
|
516
|
+
const isMac = process.platform === "darwin";
|
|
517
|
+
const isWin = process.platform === "win32";
|
|
518
|
+
switch (cmd) {
|
|
519
|
+
case "node":
|
|
520
|
+
return isMac ? "brew install node" : isWin ? "winget install NodeJS" : "apt install nodejs";
|
|
521
|
+
case "python3":
|
|
522
|
+
case "python":
|
|
523
|
+
return isMac ? "brew install python" : isWin ? "winget install Python" : "apt install python3";
|
|
524
|
+
case "docker":
|
|
525
|
+
return isMac ? "brew install --cask docker" : isWin ? "winget install Docker.DockerDesktop" : "apt install docker.io";
|
|
526
|
+
default:
|
|
527
|
+
return `Install ${cmd} and ensure it's on your PATH`;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function checkEnvVars(env) {
|
|
531
|
+
if (!env || Object.keys(env).length === 0) {
|
|
532
|
+
return { name: "Env vars", passed: true, message: "none required" };
|
|
533
|
+
}
|
|
534
|
+
const missing = [];
|
|
535
|
+
const placeholder = [];
|
|
536
|
+
const PLACEHOLDER_RE = /^(your[-_]|<|TODO|PLACEHOLDER|CHANGEME|xxx)/i;
|
|
537
|
+
for (const [key, value] of Object.entries(env)) {
|
|
538
|
+
const resolved = process.env[key] ?? value;
|
|
539
|
+
if (!resolved) {
|
|
540
|
+
missing.push(key);
|
|
541
|
+
} else if (PLACEHOLDER_RE.test(resolved)) {
|
|
542
|
+
placeholder.push(key);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (missing.length > 0) {
|
|
546
|
+
return {
|
|
547
|
+
name: "Env vars",
|
|
548
|
+
passed: false,
|
|
549
|
+
message: `missing: ${missing.join(", ")}`,
|
|
550
|
+
fix: `Set environment variables: ${missing.map((k) => `${k}=<value>`).join(" ")}`
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
if (placeholder.length > 0) {
|
|
554
|
+
return {
|
|
555
|
+
name: "Env vars",
|
|
556
|
+
passed: true,
|
|
557
|
+
message: `may have placeholder values: ${placeholder.join(", ")} (check config)`
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
return { name: "Env vars", passed: true, message: `${Object.keys(env).length} var(s) set` };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/core/health-checker.ts
|
|
564
|
+
async function checkServerHealth(name, config) {
|
|
565
|
+
const checks = [];
|
|
566
|
+
const runtimeCheck = await checkRuntime(config.command);
|
|
567
|
+
checks.push(runtimeCheck);
|
|
568
|
+
if (!runtimeCheck.passed) {
|
|
569
|
+
checks.push({ name: "Process", passed: false, skipped: true, message: "skipped (runtime missing)" });
|
|
570
|
+
checks.push({ name: "MCP handshake", passed: false, skipped: true, message: "skipped (runtime missing)" });
|
|
571
|
+
} else {
|
|
572
|
+
const spawnCheck = await checkProcessSpawn(
|
|
573
|
+
config.command,
|
|
574
|
+
config.args ?? [],
|
|
575
|
+
config.env ?? {}
|
|
576
|
+
);
|
|
577
|
+
checks.push(spawnCheck);
|
|
578
|
+
if (!spawnCheck.passed) {
|
|
579
|
+
checks.push({ name: "MCP handshake", passed: false, skipped: true, message: "skipped (process failed)" });
|
|
580
|
+
} else {
|
|
581
|
+
const handshakeCheck = await checkMcpHandshake(
|
|
582
|
+
config.command,
|
|
583
|
+
config.args ?? [],
|
|
584
|
+
config.env ?? {}
|
|
585
|
+
);
|
|
586
|
+
checks.push(handshakeCheck);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
checks.push(checkEnvVars(config.env));
|
|
590
|
+
const failed = checks.filter((c) => !c.skipped && !c.passed);
|
|
591
|
+
const status = failed.length === 0 ? "healthy" : "unhealthy";
|
|
592
|
+
return { serverName: name, status, checks };
|
|
593
|
+
}
|
|
594
|
+
async function quickHealthProbe(config, timeoutMs = 3e3) {
|
|
595
|
+
try {
|
|
596
|
+
const runtimeCheck = await checkRuntime(config.command);
|
|
597
|
+
if (!runtimeCheck.passed) return "unhealthy";
|
|
598
|
+
const handshake = await checkMcpHandshake(
|
|
599
|
+
config.command,
|
|
600
|
+
config.args ?? [],
|
|
601
|
+
config.env ?? {},
|
|
602
|
+
timeoutMs
|
|
603
|
+
);
|
|
604
|
+
return handshake.passed ? "healthy" : "unhealthy";
|
|
605
|
+
} catch {
|
|
606
|
+
return "unknown";
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/commands/doctor.ts
|
|
611
|
+
var CHECK_ICON = {
|
|
612
|
+
pass: import_picocolors.default.green("\u2713"),
|
|
613
|
+
fail: import_picocolors.default.red("\u2717"),
|
|
614
|
+
skip: import_picocolors.default.dim("-"),
|
|
615
|
+
warn: import_picocolors.default.yellow("\u26A0")
|
|
616
|
+
};
|
|
617
|
+
var doctor_default = (0, import_citty.defineCommand)({
|
|
618
|
+
meta: {
|
|
619
|
+
name: "doctor",
|
|
620
|
+
description: "Check MCP server health and configuration"
|
|
621
|
+
},
|
|
622
|
+
args: {
|
|
623
|
+
fix: {
|
|
624
|
+
type: "boolean",
|
|
625
|
+
description: "Show fix suggestions for detected issues",
|
|
626
|
+
default: false
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
async run({ args }) {
|
|
630
|
+
console.log(import_picocolors.default.bold("\n mcpman doctor\n"));
|
|
631
|
+
const servers = await getInstalledServers();
|
|
632
|
+
if (servers.length === 0) {
|
|
633
|
+
console.log(import_picocolors.default.dim(" No MCP servers installed. Run mcpman install <server> to get started."));
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const tasks = servers.map((s) => () => checkServerHealth(s.name, s.config));
|
|
637
|
+
const results = await runParallel(tasks, 5);
|
|
638
|
+
let passed = 0;
|
|
639
|
+
let failed = 0;
|
|
640
|
+
for (const result of results) {
|
|
641
|
+
printServerResult(result, args.fix);
|
|
642
|
+
if (result.status === "healthy") passed++;
|
|
643
|
+
else failed++;
|
|
644
|
+
}
|
|
645
|
+
console.log(import_picocolors.default.dim(" " + "\u2500".repeat(50)));
|
|
646
|
+
const parts = [];
|
|
647
|
+
if (passed > 0) parts.push(import_picocolors.default.green(`${passed} healthy`));
|
|
648
|
+
if (failed > 0) parts.push(import_picocolors.default.red(`${failed} unhealthy`));
|
|
649
|
+
console.log(` Summary: ${parts.join(", ")}`);
|
|
650
|
+
if (failed > 0) {
|
|
651
|
+
if (!args.fix) {
|
|
652
|
+
console.log(import_picocolors.default.dim(` Run ${import_picocolors.default.cyan("mcpman doctor --fix")} for fix suggestions.
|
|
653
|
+
`));
|
|
654
|
+
}
|
|
655
|
+
process.exit(1);
|
|
656
|
+
}
|
|
657
|
+
console.log();
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
function printServerResult(result, showFix) {
|
|
661
|
+
const icon = result.status === "healthy" ? import_picocolors.default.green("\u25CF") : import_picocolors.default.red("\u25CF");
|
|
662
|
+
console.log(` ${icon} ${import_picocolors.default.bold(result.serverName)}`);
|
|
663
|
+
for (const check of result.checks) {
|
|
664
|
+
const checkIcon = check.skipped ? CHECK_ICON.skip : check.passed ? CHECK_ICON.pass : CHECK_ICON.fail;
|
|
665
|
+
console.log(` ${checkIcon} ${check.name}: ${check.message}`);
|
|
666
|
+
if (showFix && !check.passed && !check.skipped && check.fix) {
|
|
667
|
+
console.log(` ${import_picocolors.default.yellow("\u2192")} Fix: ${import_picocolors.default.cyan(check.fix)}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
console.log();
|
|
671
|
+
}
|
|
672
|
+
async function runParallel(tasks, concurrency) {
|
|
673
|
+
const results = [];
|
|
674
|
+
const executing = /* @__PURE__ */ new Set();
|
|
675
|
+
for (const task of tasks) {
|
|
676
|
+
const p5 = task().then((r) => {
|
|
677
|
+
results.push(r);
|
|
678
|
+
executing.delete(p5);
|
|
679
|
+
});
|
|
680
|
+
executing.add(p5);
|
|
681
|
+
if (executing.size >= concurrency) {
|
|
682
|
+
await Promise.race(executing);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
await Promise.all(executing);
|
|
686
|
+
return results;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/commands/init.ts
|
|
690
|
+
init_cjs_shims();
|
|
691
|
+
var import_citty2 = require("citty");
|
|
692
|
+
var p = __toESM(require("@clack/prompts"), 1);
|
|
693
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
694
|
+
|
|
695
|
+
// src/core/lockfile.ts
|
|
696
|
+
init_cjs_shims();
|
|
697
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
698
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
699
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
700
|
+
var LOCKFILE_NAME = "mcpman.lock";
|
|
701
|
+
function findLockfile() {
|
|
702
|
+
let dir = process.cwd();
|
|
703
|
+
while (true) {
|
|
704
|
+
const candidate = import_node_path3.default.join(dir, LOCKFILE_NAME);
|
|
705
|
+
if (import_node_fs2.default.existsSync(candidate)) return candidate;
|
|
706
|
+
const parent = import_node_path3.default.dirname(dir);
|
|
707
|
+
if (parent === dir) break;
|
|
708
|
+
dir = parent;
|
|
709
|
+
}
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
function getGlobalLockfilePath() {
|
|
713
|
+
return import_node_path3.default.join(import_node_os2.default.homedir(), ".mcpman", LOCKFILE_NAME);
|
|
714
|
+
}
|
|
715
|
+
function resolveLockfilePath() {
|
|
716
|
+
return findLockfile() ?? getGlobalLockfilePath();
|
|
717
|
+
}
|
|
718
|
+
function readLockfile(filePath) {
|
|
719
|
+
const target = filePath ?? resolveLockfilePath();
|
|
720
|
+
if (!import_node_fs2.default.existsSync(target)) {
|
|
721
|
+
return { lockfileVersion: 1, servers: {} };
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
const raw = import_node_fs2.default.readFileSync(target, "utf-8");
|
|
725
|
+
return JSON.parse(raw);
|
|
726
|
+
} catch {
|
|
727
|
+
return { lockfileVersion: 1, servers: {} };
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function serialize(data) {
|
|
731
|
+
const sorted = {
|
|
732
|
+
lockfileVersion: data.lockfileVersion,
|
|
733
|
+
servers: Object.fromEntries(
|
|
734
|
+
Object.entries(data.servers).sort(([a], [b]) => a.localeCompare(b))
|
|
735
|
+
)
|
|
736
|
+
};
|
|
737
|
+
return JSON.stringify(sorted, null, 2) + "\n";
|
|
738
|
+
}
|
|
739
|
+
function writeLockfile(data, filePath) {
|
|
740
|
+
const target = filePath ?? resolveLockfilePath();
|
|
741
|
+
const dir = import_node_path3.default.dirname(target);
|
|
742
|
+
if (!import_node_fs2.default.existsSync(dir)) {
|
|
743
|
+
import_node_fs2.default.mkdirSync(dir, { recursive: true });
|
|
744
|
+
}
|
|
745
|
+
const tmp = `${target}.tmp`;
|
|
746
|
+
import_node_fs2.default.writeFileSync(tmp, serialize(data), "utf-8");
|
|
747
|
+
import_node_fs2.default.renameSync(tmp, target);
|
|
748
|
+
}
|
|
749
|
+
function addEntry(name, entry, filePath) {
|
|
750
|
+
const data = readLockfile(filePath);
|
|
751
|
+
data.servers[name] = entry;
|
|
752
|
+
writeLockfile(data, filePath);
|
|
753
|
+
}
|
|
754
|
+
function createEmptyLockfile(filePath) {
|
|
755
|
+
writeLockfile({ lockfileVersion: 1, servers: {} }, filePath);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/core/registry.ts
|
|
759
|
+
init_cjs_shims();
|
|
760
|
+
var import_node_crypto = require("crypto");
|
|
761
|
+
function computeIntegrity(resolvedUrl) {
|
|
762
|
+
const hash = (0, import_node_crypto.createHash)("sha512").update(resolvedUrl).digest("base64");
|
|
763
|
+
return `sha512-${hash}`;
|
|
764
|
+
}
|
|
765
|
+
async function resolveFromSmithery(name) {
|
|
766
|
+
const url = `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`;
|
|
767
|
+
let data;
|
|
768
|
+
try {
|
|
769
|
+
const res = await fetch(url, {
|
|
770
|
+
headers: { Accept: "application/json" },
|
|
771
|
+
signal: AbortSignal.timeout(8e3)
|
|
772
|
+
});
|
|
773
|
+
if (res.status === 404) {
|
|
774
|
+
throw new Error(`Server '${name}' not found on Smithery registry`);
|
|
775
|
+
}
|
|
776
|
+
if (!res.ok) {
|
|
777
|
+
throw new Error(`Smithery API error: ${res.status}`);
|
|
778
|
+
}
|
|
779
|
+
data = await res.json();
|
|
780
|
+
} catch (err) {
|
|
781
|
+
if (err instanceof Error && err.message.includes("not found")) throw err;
|
|
782
|
+
throw new Error(
|
|
783
|
+
`Cannot reach Smithery registry: ${err instanceof Error ? err.message : String(err)}`
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
const version = typeof data.version === "string" ? data.version : "latest";
|
|
787
|
+
const command = typeof data.command === "string" ? data.command : "npx";
|
|
788
|
+
const args = Array.isArray(data.args) ? data.args : ["-y", `${name}@${version}`];
|
|
789
|
+
const envVars = Array.isArray(data.envVars) ? data.envVars : [];
|
|
790
|
+
const resolved = typeof data.resolved === "string" ? data.resolved : `smithery:${name}@${version}`;
|
|
791
|
+
return {
|
|
792
|
+
name,
|
|
793
|
+
version,
|
|
794
|
+
description: typeof data.description === "string" ? data.description : "",
|
|
795
|
+
runtime: "node",
|
|
796
|
+
command,
|
|
797
|
+
args,
|
|
798
|
+
envVars,
|
|
799
|
+
resolved
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
async function resolveFromNpm(packageName) {
|
|
803
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
|
|
804
|
+
let data;
|
|
805
|
+
try {
|
|
806
|
+
const res = await fetch(url, {
|
|
807
|
+
headers: { Accept: "application/json" },
|
|
808
|
+
signal: AbortSignal.timeout(8e3)
|
|
809
|
+
});
|
|
810
|
+
if (res.status === 404) {
|
|
811
|
+
throw new Error(`Package '${packageName}' not found on npm`);
|
|
812
|
+
}
|
|
813
|
+
if (!res.ok) {
|
|
814
|
+
throw new Error(`npm registry error: ${res.status}`);
|
|
815
|
+
}
|
|
816
|
+
data = await res.json();
|
|
817
|
+
} catch (err) {
|
|
818
|
+
if (err instanceof Error && err.message.includes("not found")) throw err;
|
|
819
|
+
throw new Error(
|
|
820
|
+
`Cannot reach npm registry: ${err instanceof Error ? err.message : String(err)}`
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const version = typeof data.version === "string" ? data.version : "latest";
|
|
824
|
+
const resolved = `https://registry.npmjs.org/${packageName}/-/${packageName.replace(/^@[^/]+\//, "")}-${version}.tgz`;
|
|
825
|
+
const mcpField = data.mcp && typeof data.mcp === "object" ? data.mcp : null;
|
|
826
|
+
const envVars = mcpField?.envVars ? mcpField.envVars : [];
|
|
827
|
+
return {
|
|
828
|
+
name: packageName,
|
|
829
|
+
version,
|
|
830
|
+
description: typeof data.description === "string" ? data.description : "",
|
|
831
|
+
runtime: "node",
|
|
832
|
+
command: "npx",
|
|
833
|
+
args: ["-y", `${packageName}@${version}`],
|
|
834
|
+
envVars,
|
|
835
|
+
resolved
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
async function resolveFromGitHub(githubUrl) {
|
|
839
|
+
const match = githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
840
|
+
if (!match) {
|
|
841
|
+
throw new Error(`Invalid GitHub URL: ${githubUrl}`);
|
|
842
|
+
}
|
|
843
|
+
const [, owner, repo] = match;
|
|
844
|
+
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/package.json`;
|
|
845
|
+
let pkgData = {};
|
|
846
|
+
try {
|
|
847
|
+
const res = await fetch(rawUrl, { signal: AbortSignal.timeout(8e3) });
|
|
848
|
+
if (res.ok) {
|
|
849
|
+
pkgData = await res.json();
|
|
850
|
+
}
|
|
851
|
+
} catch {
|
|
852
|
+
}
|
|
853
|
+
const version = typeof pkgData.version === "string" ? pkgData.version : "main";
|
|
854
|
+
const name = typeof pkgData.name === "string" ? pkgData.name : `${owner}/${repo}`;
|
|
855
|
+
return {
|
|
856
|
+
name,
|
|
857
|
+
version,
|
|
858
|
+
description: typeof pkgData.description === "string" ? pkgData.description : "",
|
|
859
|
+
runtime: "node",
|
|
860
|
+
command: "npx",
|
|
861
|
+
args: ["-y", githubUrl],
|
|
862
|
+
envVars: [],
|
|
863
|
+
resolved: githubUrl
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/commands/init.ts
|
|
868
|
+
var init_default = (0, import_citty2.defineCommand)({
|
|
869
|
+
meta: {
|
|
870
|
+
name: "init",
|
|
871
|
+
description: "Initialize mcpman.lock in the current project"
|
|
872
|
+
},
|
|
873
|
+
args: {},
|
|
874
|
+
async run() {
|
|
875
|
+
p.intro("mcpman init");
|
|
876
|
+
const targetPath = import_node_path4.default.join(process.cwd(), LOCKFILE_NAME);
|
|
877
|
+
const existing = findLockfile();
|
|
878
|
+
if (existing) {
|
|
879
|
+
p.log.warn(`Lockfile already exists: ${existing}`);
|
|
880
|
+
const overwrite = await p.confirm({ message: "Overwrite?" });
|
|
881
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
882
|
+
p.outro("Cancelled.");
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
let clients = [];
|
|
887
|
+
try {
|
|
888
|
+
const mod = await Promise.resolve().then(() => (init_client_detector(), client_detector_exports));
|
|
889
|
+
clients = await mod.getInstalledClients();
|
|
890
|
+
} catch {
|
|
891
|
+
p.log.warn("Could not detect AI clients \u2014 creating empty lockfile.");
|
|
892
|
+
}
|
|
893
|
+
const clientServers = [];
|
|
894
|
+
for (const client of clients) {
|
|
895
|
+
try {
|
|
896
|
+
const config = await client.readConfig();
|
|
897
|
+
if (Object.keys(config.servers).length > 0) {
|
|
898
|
+
clientServers.push({ client, servers: config.servers });
|
|
899
|
+
}
|
|
900
|
+
} catch {
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
createEmptyLockfile(targetPath);
|
|
904
|
+
if (clientServers.length === 0) {
|
|
905
|
+
p.log.info("No existing servers found in any client config.");
|
|
906
|
+
p.outro(`Created ${LOCKFILE_NAME} \u2014 add it to version control!`);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const options = clientServers.map((cs) => ({
|
|
910
|
+
value: cs.client.type,
|
|
911
|
+
label: `${cs.client.displayName} (${Object.keys(cs.servers).length} servers)`
|
|
912
|
+
}));
|
|
913
|
+
const toImport = await p.multiselect({
|
|
914
|
+
message: "Import existing servers into lockfile?",
|
|
915
|
+
options,
|
|
916
|
+
required: false
|
|
917
|
+
});
|
|
918
|
+
if (p.isCancel(toImport)) {
|
|
919
|
+
p.outro(`Created empty ${LOCKFILE_NAME}`);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const selected = toImport;
|
|
923
|
+
let importCount = 0;
|
|
924
|
+
for (const cs of clientServers) {
|
|
925
|
+
if (!selected.includes(cs.client.type)) continue;
|
|
926
|
+
for (const [name, entry] of Object.entries(cs.servers)) {
|
|
927
|
+
const resolved = `npm:${name}`;
|
|
928
|
+
const lockEntry = {
|
|
929
|
+
version: "unknown",
|
|
930
|
+
source: "npm",
|
|
931
|
+
resolved,
|
|
932
|
+
integrity: computeIntegrity(resolved),
|
|
933
|
+
runtime: "node",
|
|
934
|
+
command: entry.command,
|
|
935
|
+
args: entry.args ?? [],
|
|
936
|
+
envVars: Object.keys(entry.env ?? {}),
|
|
937
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
938
|
+
clients: [cs.client.type]
|
|
939
|
+
};
|
|
940
|
+
addEntry(name, lockEntry, targetPath);
|
|
941
|
+
importCount++;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
p.outro(
|
|
945
|
+
`Created ${LOCKFILE_NAME} with ${importCount} server(s) \u2014 commit to version control!`
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
// src/commands/install.ts
|
|
951
|
+
init_cjs_shims();
|
|
952
|
+
var import_citty3 = require("citty");
|
|
953
|
+
|
|
954
|
+
// src/core/installer.ts
|
|
955
|
+
init_cjs_shims();
|
|
956
|
+
var p2 = __toESM(require("@clack/prompts"), 1);
|
|
957
|
+
|
|
958
|
+
// src/core/server-resolver.ts
|
|
959
|
+
init_cjs_shims();
|
|
960
|
+
function detectSource(input) {
|
|
961
|
+
if (input.startsWith("smithery:")) {
|
|
962
|
+
return { type: "smithery", input: input.slice(9) };
|
|
963
|
+
}
|
|
964
|
+
if (input.startsWith("https://github.com/") || input.startsWith("github.com/")) {
|
|
965
|
+
return { type: "github", input };
|
|
966
|
+
}
|
|
967
|
+
return { type: "npm", input };
|
|
968
|
+
}
|
|
969
|
+
function parseEnvFlags(envFlags) {
|
|
970
|
+
if (!envFlags) return {};
|
|
971
|
+
const flags = Array.isArray(envFlags) ? envFlags : [envFlags];
|
|
972
|
+
const result = {};
|
|
973
|
+
for (const flag of flags) {
|
|
974
|
+
const idx = flag.indexOf("=");
|
|
975
|
+
if (idx > 0) {
|
|
976
|
+
result[flag.slice(0, idx)] = flag.slice(idx + 1);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return result;
|
|
980
|
+
}
|
|
981
|
+
async function resolveServer(input) {
|
|
982
|
+
const source = detectSource(input);
|
|
983
|
+
switch (source.type) {
|
|
984
|
+
case "smithery":
|
|
985
|
+
return resolveFromSmithery(source.input);
|
|
986
|
+
case "github":
|
|
987
|
+
return resolveFromGitHub(source.input);
|
|
988
|
+
case "npm":
|
|
989
|
+
return resolveFromNpm(source.input);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// src/core/installer.ts
|
|
994
|
+
async function loadClients() {
|
|
995
|
+
try {
|
|
996
|
+
const mod = await Promise.resolve().then(() => (init_client_detector(), client_detector_exports));
|
|
997
|
+
return mod.getInstalledClients();
|
|
998
|
+
} catch {
|
|
999
|
+
return [];
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
async function installServer(input, options = {}) {
|
|
1003
|
+
p2.intro("mcpman install");
|
|
1004
|
+
const spinner2 = p2.spinner();
|
|
1005
|
+
spinner2.start("Resolving server...");
|
|
1006
|
+
let metadata;
|
|
1007
|
+
try {
|
|
1008
|
+
metadata = await resolveServer(input);
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
spinner2.stop("Resolution failed");
|
|
1011
|
+
p2.log.error(err instanceof Error ? err.message : String(err));
|
|
1012
|
+
process.exit(1);
|
|
1013
|
+
}
|
|
1014
|
+
spinner2.stop(`Found: ${metadata.name}@${metadata.version}`);
|
|
1015
|
+
const clients = await loadClients();
|
|
1016
|
+
if (clients.length === 0) {
|
|
1017
|
+
p2.log.warn("No supported AI clients detected on this machine.");
|
|
1018
|
+
p2.log.info("Supported: Claude Desktop, Cursor, VS Code, Windsurf");
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
}
|
|
1021
|
+
let selectedClients;
|
|
1022
|
+
if (options.client) {
|
|
1023
|
+
const found = clients.find((c) => c.type === options.client || c.displayName.toLowerCase() === options.client?.toLowerCase());
|
|
1024
|
+
if (!found) {
|
|
1025
|
+
p2.log.error(`Client '${options.client}' not found or not installed.`);
|
|
1026
|
+
p2.log.info(`Available: ${clients.map((c) => c.type).join(", ")}`);
|
|
1027
|
+
process.exit(1);
|
|
1028
|
+
}
|
|
1029
|
+
selectedClients = [found];
|
|
1030
|
+
} else if (options.yes || clients.length === 1) {
|
|
1031
|
+
selectedClients = clients;
|
|
1032
|
+
} else {
|
|
1033
|
+
const chosen = await p2.multiselect({
|
|
1034
|
+
message: "Install to which client(s)?",
|
|
1035
|
+
options: clients.map((c) => ({ value: c.type, label: c.displayName })),
|
|
1036
|
+
required: true
|
|
1037
|
+
});
|
|
1038
|
+
if (p2.isCancel(chosen)) {
|
|
1039
|
+
p2.outro("Cancelled.");
|
|
1040
|
+
process.exit(0);
|
|
1041
|
+
}
|
|
1042
|
+
selectedClients = clients.filter((c) => chosen.includes(c.type));
|
|
1043
|
+
}
|
|
1044
|
+
const providedEnv = parseEnvFlags(options.env);
|
|
1045
|
+
const collectedEnv = { ...providedEnv };
|
|
1046
|
+
const requiredVars = metadata.envVars.filter((e) => e.required && !(e.name in collectedEnv));
|
|
1047
|
+
for (const envVar of requiredVars) {
|
|
1048
|
+
if (options.yes && envVar.default) {
|
|
1049
|
+
collectedEnv[envVar.name] = envVar.default;
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
const val = await p2.text({
|
|
1053
|
+
message: `${envVar.name}${envVar.description ? ` \u2014 ${envVar.description}` : ""}`,
|
|
1054
|
+
placeholder: envVar.default ?? "",
|
|
1055
|
+
validate: (v) => envVar.required && !v ? "Required" : void 0
|
|
1056
|
+
});
|
|
1057
|
+
if (p2.isCancel(val)) {
|
|
1058
|
+
p2.outro("Cancelled.");
|
|
1059
|
+
process.exit(0);
|
|
1060
|
+
}
|
|
1061
|
+
collectedEnv[envVar.name] = val;
|
|
1062
|
+
}
|
|
1063
|
+
const entry = {
|
|
1064
|
+
command: metadata.command,
|
|
1065
|
+
args: metadata.args,
|
|
1066
|
+
...Object.keys(collectedEnv).length > 0 ? { env: collectedEnv } : {}
|
|
1067
|
+
};
|
|
1068
|
+
spinner2.start("Writing config...");
|
|
1069
|
+
const clientTypes = [];
|
|
1070
|
+
for (const client of selectedClients) {
|
|
1071
|
+
try {
|
|
1072
|
+
await client.addServer(metadata.name, entry);
|
|
1073
|
+
clientTypes.push(client.type);
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
spinner2.stop("Partial failure");
|
|
1076
|
+
p2.log.warn(`Failed to write to ${client.displayName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
spinner2.stop("Config written");
|
|
1080
|
+
const source = detectSource(input);
|
|
1081
|
+
const integrity = computeIntegrity(metadata.resolved);
|
|
1082
|
+
addEntry(metadata.name, {
|
|
1083
|
+
version: metadata.version,
|
|
1084
|
+
source: source.type,
|
|
1085
|
+
resolved: metadata.resolved,
|
|
1086
|
+
integrity,
|
|
1087
|
+
runtime: metadata.runtime,
|
|
1088
|
+
command: metadata.command,
|
|
1089
|
+
args: metadata.args,
|
|
1090
|
+
envVars: metadata.envVars.map((e) => e.name),
|
|
1091
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1092
|
+
clients: clientTypes
|
|
1093
|
+
});
|
|
1094
|
+
const lockPath = findLockfile() ?? "mcpman.lock (global)";
|
|
1095
|
+
p2.log.success(`Lockfile updated: ${lockPath}`);
|
|
1096
|
+
p2.outro(`${metadata.name}@${metadata.version} installed to ${clientTypes.join(", ")}`);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// src/utils/logger.ts
|
|
1100
|
+
init_cjs_shims();
|
|
1101
|
+
var import_picocolors2 = __toESM(require("picocolors"), 1);
|
|
1102
|
+
var noColor = process.env.NO_COLOR !== void 0 || process.argv.includes("--no-color");
|
|
1103
|
+
var isVerbose = process.argv.includes("--verbose");
|
|
1104
|
+
var isJson = process.argv.includes("--json");
|
|
1105
|
+
function colorize(fn, text2) {
|
|
1106
|
+
return noColor ? text2 : fn(text2);
|
|
1107
|
+
}
|
|
1108
|
+
function info(message) {
|
|
1109
|
+
if (isJson) return;
|
|
1110
|
+
console.log(`${colorize(import_picocolors2.default.cyan, "i")} ${message}`);
|
|
1111
|
+
}
|
|
1112
|
+
function error(message) {
|
|
1113
|
+
if (isJson) return;
|
|
1114
|
+
console.error(`${colorize(import_picocolors2.default.red, "\u2717")} ${message}`);
|
|
1115
|
+
}
|
|
1116
|
+
function json(data) {
|
|
1117
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/commands/install.ts
|
|
1121
|
+
var p3 = __toESM(require("@clack/prompts"), 1);
|
|
1122
|
+
var install_default = (0, import_citty3.defineCommand)({
|
|
1123
|
+
meta: {
|
|
1124
|
+
name: "install",
|
|
1125
|
+
description: "Install an MCP server into one or more AI clients"
|
|
1126
|
+
},
|
|
1127
|
+
args: {
|
|
1128
|
+
server: {
|
|
1129
|
+
type: "positional",
|
|
1130
|
+
description: "Server name or package (e.g. @modelcontextprotocol/server-github, smithery:github)",
|
|
1131
|
+
required: false
|
|
1132
|
+
},
|
|
1133
|
+
client: {
|
|
1134
|
+
type: "string",
|
|
1135
|
+
description: "Target client (claude-desktop, cursor, vscode, windsurf)"
|
|
1136
|
+
},
|
|
1137
|
+
env: {
|
|
1138
|
+
type: "string",
|
|
1139
|
+
description: "Environment variable KEY=VAL (can repeat)"
|
|
1140
|
+
},
|
|
1141
|
+
yes: {
|
|
1142
|
+
type: "boolean",
|
|
1143
|
+
description: "Skip confirmation prompts",
|
|
1144
|
+
default: false
|
|
1145
|
+
}
|
|
1146
|
+
},
|
|
1147
|
+
async run({ args }) {
|
|
1148
|
+
if (!args.server) {
|
|
1149
|
+
await restoreFromLockfile();
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
await installServer(args.server, {
|
|
1153
|
+
client: args.client,
|
|
1154
|
+
env: args.env,
|
|
1155
|
+
yes: args.yes
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
async function restoreFromLockfile() {
|
|
1160
|
+
const lockPath = findLockfile();
|
|
1161
|
+
if (!lockPath) {
|
|
1162
|
+
error("No mcpman.lock found. Run 'mcpman init' first or provide a server name.");
|
|
1163
|
+
process.exit(1);
|
|
1164
|
+
}
|
|
1165
|
+
const lockfile = readLockfile(lockPath);
|
|
1166
|
+
const entries = Object.entries(lockfile.servers);
|
|
1167
|
+
if (entries.length === 0) {
|
|
1168
|
+
info("Lockfile is empty \u2014 nothing to restore.");
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
p3.intro(`mcpman install (restore from ${lockPath})`);
|
|
1172
|
+
p3.log.info(`Restoring ${entries.length} server(s)...`);
|
|
1173
|
+
for (const [name, entry] of entries) {
|
|
1174
|
+
const input = entry.source === "smithery" ? `smithery:${name}` : entry.source === "github" ? entry.resolved : name;
|
|
1175
|
+
await installServer(input, {
|
|
1176
|
+
client: entry.clients[0],
|
|
1177
|
+
yes: true
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
p3.outro("Restore complete.");
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// src/commands/list.ts
|
|
1184
|
+
init_cjs_shims();
|
|
1185
|
+
var import_citty4 = require("citty");
|
|
1186
|
+
var import_picocolors3 = __toESM(require("picocolors"), 1);
|
|
1187
|
+
var STATUS_ICON = {
|
|
1188
|
+
healthy: import_picocolors3.default.green("\u25CF"),
|
|
1189
|
+
unhealthy: import_picocolors3.default.red("\u25CF"),
|
|
1190
|
+
unknown: import_picocolors3.default.dim("\u25CB")
|
|
1191
|
+
};
|
|
1192
|
+
var list_default = (0, import_citty4.defineCommand)({
|
|
1193
|
+
meta: {
|
|
1194
|
+
name: "list",
|
|
1195
|
+
description: "List installed MCP servers"
|
|
1196
|
+
},
|
|
1197
|
+
args: {
|
|
1198
|
+
client: {
|
|
1199
|
+
type: "string",
|
|
1200
|
+
description: "Filter by client (claude, cursor, vscode, windsurf)"
|
|
1201
|
+
},
|
|
1202
|
+
json: {
|
|
1203
|
+
type: "boolean",
|
|
1204
|
+
description: "Output as JSON",
|
|
1205
|
+
default: false
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
async run({ args }) {
|
|
1209
|
+
const servers = await getInstalledServers(args.client);
|
|
1210
|
+
if (servers.length === 0) {
|
|
1211
|
+
const filter = args.client ? ` for client "${args.client}"` : "";
|
|
1212
|
+
console.log(import_picocolors3.default.dim(`No MCP servers installed${filter}. Run ${import_picocolors3.default.cyan("mcpman install <server>")} to get started.`));
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
const withStatus = await Promise.all(
|
|
1216
|
+
servers.map(async (s) => ({
|
|
1217
|
+
...s,
|
|
1218
|
+
status: await quickHealthProbe(s.config, 3e3)
|
|
1219
|
+
}))
|
|
1220
|
+
);
|
|
1221
|
+
if (args.json) {
|
|
1222
|
+
json({
|
|
1223
|
+
servers: withStatus.map((s) => ({
|
|
1224
|
+
name: s.name,
|
|
1225
|
+
clients: s.clients,
|
|
1226
|
+
status: s.status,
|
|
1227
|
+
config: s.config
|
|
1228
|
+
})),
|
|
1229
|
+
total: withStatus.length
|
|
1230
|
+
});
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const nameWidth = Math.max(4, ...withStatus.map((s) => s.name.length));
|
|
1234
|
+
const clientsWidth = Math.max(7, ...withStatus.map((s) => formatClients(s.clients).length));
|
|
1235
|
+
const header = ` ${pad("NAME", nameWidth)} ${pad("CLIENT(S)", clientsWidth)} ${pad("COMMAND", 20)} STATUS`;
|
|
1236
|
+
console.log(import_picocolors3.default.dim(header));
|
|
1237
|
+
console.log(import_picocolors3.default.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientsWidth)} ${"-".repeat(20)} ------`));
|
|
1238
|
+
for (const s of withStatus) {
|
|
1239
|
+
const icon = STATUS_ICON[s.status];
|
|
1240
|
+
const clientsStr = formatClients(s.clients);
|
|
1241
|
+
const cmdStr = truncate(`${s.config.command}${s.config.args ? " " + s.config.args.join(" ") : ""}`, 20);
|
|
1242
|
+
console.log(` ${pad(s.name, nameWidth)} ${pad(clientsStr, clientsWidth)} ${pad(cmdStr, 20)} ${icon} ${s.status}`);
|
|
1243
|
+
}
|
|
1244
|
+
const clientSet = new Set(withStatus.flatMap((s) => s.clients));
|
|
1245
|
+
console.log(import_picocolors3.default.dim(`
|
|
1246
|
+
${withStatus.length} server${withStatus.length !== 1 ? "s" : ""} \xB7 ${clientSet.size} client${clientSet.size !== 1 ? "s" : ""}`));
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
function pad(s, width) {
|
|
1250
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
1251
|
+
}
|
|
1252
|
+
function truncate(s, max) {
|
|
1253
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
1254
|
+
}
|
|
1255
|
+
var CLIENT_DISPLAY = {
|
|
1256
|
+
"claude-desktop": "Claude",
|
|
1257
|
+
cursor: "Cursor",
|
|
1258
|
+
vscode: "VS Code",
|
|
1259
|
+
windsurf: "Windsurf"
|
|
1260
|
+
};
|
|
1261
|
+
function formatClients(clients) {
|
|
1262
|
+
return clients.map((c) => CLIENT_DISPLAY[c] ?? c).join(", ");
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// src/commands/remove.ts
|
|
1266
|
+
init_cjs_shims();
|
|
1267
|
+
var import_citty5 = require("citty");
|
|
1268
|
+
var p4 = __toESM(require("@clack/prompts"), 1);
|
|
1269
|
+
var import_picocolors4 = __toESM(require("picocolors"), 1);
|
|
1270
|
+
init_client_detector();
|
|
1271
|
+
var CLIENT_DISPLAY2 = {
|
|
1272
|
+
"claude-desktop": "Claude",
|
|
1273
|
+
cursor: "Cursor",
|
|
1274
|
+
vscode: "VS Code",
|
|
1275
|
+
windsurf: "Windsurf"
|
|
1276
|
+
};
|
|
1277
|
+
function clientDisplayName(type) {
|
|
1278
|
+
return CLIENT_DISPLAY2[type] ?? type;
|
|
1279
|
+
}
|
|
1280
|
+
var remove_default = (0, import_citty5.defineCommand)({
|
|
1281
|
+
meta: {
|
|
1282
|
+
name: "remove",
|
|
1283
|
+
description: "Remove an MCP server from one or more AI clients"
|
|
1284
|
+
},
|
|
1285
|
+
args: {
|
|
1286
|
+
server: {
|
|
1287
|
+
type: "positional",
|
|
1288
|
+
description: "Server name to remove",
|
|
1289
|
+
required: true
|
|
1290
|
+
},
|
|
1291
|
+
client: {
|
|
1292
|
+
type: "string",
|
|
1293
|
+
description: "Target client (claude, cursor, vscode, windsurf)"
|
|
1294
|
+
},
|
|
1295
|
+
all: {
|
|
1296
|
+
type: "boolean",
|
|
1297
|
+
description: "Remove from all clients",
|
|
1298
|
+
default: false
|
|
1299
|
+
},
|
|
1300
|
+
yes: {
|
|
1301
|
+
type: "boolean",
|
|
1302
|
+
description: "Skip confirmation prompt",
|
|
1303
|
+
default: false
|
|
1304
|
+
}
|
|
1305
|
+
},
|
|
1306
|
+
async run({ args }) {
|
|
1307
|
+
p4.intro(import_picocolors4.default.bold("mcpman remove"));
|
|
1308
|
+
const serverName = args.server;
|
|
1309
|
+
const servers = await getInstalledServers();
|
|
1310
|
+
const match = servers.find((s) => s.name === serverName);
|
|
1311
|
+
if (!match) {
|
|
1312
|
+
p4.log.warn(`Server "${serverName}" is not installed.`);
|
|
1313
|
+
const similar = servers.filter((s) => s.name.includes(serverName) || serverName.includes(s.name));
|
|
1314
|
+
if (similar.length > 0) {
|
|
1315
|
+
p4.log.info(`Did you mean: ${similar.map((s) => import_picocolors4.default.cyan(s.name)).join(", ")}?`);
|
|
1316
|
+
}
|
|
1317
|
+
p4.outro("Nothing to remove.");
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
let targetClients;
|
|
1321
|
+
if (args.all) {
|
|
1322
|
+
targetClients = match.clients;
|
|
1323
|
+
} else if (args.client) {
|
|
1324
|
+
if (!match.clients.includes(args.client)) {
|
|
1325
|
+
p4.log.warn(`Server "${serverName}" is not installed in client "${args.client}".`);
|
|
1326
|
+
p4.outro("Nothing to remove.");
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
targetClients = [args.client];
|
|
1330
|
+
} else if (match.clients.length === 1) {
|
|
1331
|
+
targetClients = match.clients;
|
|
1332
|
+
} else {
|
|
1333
|
+
const selected = await p4.multiselect({
|
|
1334
|
+
message: `Remove "${serverName}" from which clients?`,
|
|
1335
|
+
options: match.clients.map((c) => ({
|
|
1336
|
+
value: c,
|
|
1337
|
+
label: clientDisplayName(c)
|
|
1338
|
+
})),
|
|
1339
|
+
required: true
|
|
1340
|
+
});
|
|
1341
|
+
if (p4.isCancel(selected)) {
|
|
1342
|
+
p4.outro("Cancelled.");
|
|
1343
|
+
process.exit(0);
|
|
1344
|
+
}
|
|
1345
|
+
targetClients = selected;
|
|
1346
|
+
}
|
|
1347
|
+
if (!args.yes) {
|
|
1348
|
+
const clientNames = targetClients.map(clientDisplayName).join(", ");
|
|
1349
|
+
const confirmed = await p4.confirm({
|
|
1350
|
+
message: `Remove ${import_picocolors4.default.cyan(serverName)} from ${import_picocolors4.default.yellow(clientNames)}?`
|
|
1351
|
+
});
|
|
1352
|
+
if (p4.isCancel(confirmed) || !confirmed) {
|
|
1353
|
+
p4.outro("Cancelled.");
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
const installedClients = await getInstalledClients();
|
|
1358
|
+
const errors = [];
|
|
1359
|
+
for (const clientType of targetClients) {
|
|
1360
|
+
const handler = installedClients.find((c) => c.type === clientType);
|
|
1361
|
+
if (!handler) {
|
|
1362
|
+
errors.push(`Client "${clientType}" not found`);
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
try {
|
|
1366
|
+
await handler.removeServer(serverName);
|
|
1367
|
+
p4.log.success(`Removed from ${clientDisplayName(clientType)}`);
|
|
1368
|
+
} catch (err) {
|
|
1369
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1370
|
+
errors.push(`${clientDisplayName(clientType)}: ${msg}`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
if (errors.length > 0) {
|
|
1374
|
+
for (const e of errors) p4.log.error(e);
|
|
1375
|
+
p4.outro(import_picocolors4.default.red("Completed with errors."));
|
|
1376
|
+
process.exit(1);
|
|
1377
|
+
}
|
|
1378
|
+
p4.outro(import_picocolors4.default.green(`Removed "${serverName}" successfully.`));
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
// src/utils/constants.ts
|
|
1383
|
+
init_cjs_shims();
|
|
1384
|
+
var APP_NAME = "mcpman";
|
|
1385
|
+
var APP_VERSION = "0.1.0";
|
|
1386
|
+
var APP_DESCRIPTION = "The package manager for MCP servers";
|
|
1387
|
+
|
|
1388
|
+
// src/index.ts
|
|
1389
|
+
process.on("SIGINT", () => {
|
|
1390
|
+
console.log("\nAborted.");
|
|
1391
|
+
process.exit(130);
|
|
1392
|
+
});
|
|
1393
|
+
var main = (0, import_citty6.defineCommand)({
|
|
1394
|
+
meta: {
|
|
1395
|
+
name: APP_NAME,
|
|
1396
|
+
version: APP_VERSION,
|
|
1397
|
+
description: APP_DESCRIPTION
|
|
1398
|
+
},
|
|
1399
|
+
subCommands: {
|
|
1400
|
+
install: install_default,
|
|
1401
|
+
list: list_default,
|
|
1402
|
+
remove: remove_default,
|
|
1403
|
+
doctor: doctor_default,
|
|
1404
|
+
init: init_default
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
(0, import_citty6.runMain)(main);
|