mcpman 0.1.0 → 0.2.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 +8 -2
- package/dist/chunk-6X6Q6UZC.js +141 -0
- package/dist/index.cjs +1391 -167
- package/dist/index.js +1127 -139
- package/dist/trust-scorer-LYC6KZCD.js +77 -0
- package/dist/vault-service-UTZAV6N6.js +29 -0
- package/package.json +11 -4
package/dist/index.js
CHANGED
|
@@ -2,13 +2,370 @@
|
|
|
2
2
|
import {
|
|
3
3
|
getInstalledClients
|
|
4
4
|
} from "./chunk-QY22QTBR.js";
|
|
5
|
+
import {
|
|
6
|
+
getMasterPassword,
|
|
7
|
+
listSecrets,
|
|
8
|
+
removeSecret,
|
|
9
|
+
setSecret
|
|
10
|
+
} from "./chunk-6X6Q6UZC.js";
|
|
5
11
|
|
|
6
12
|
// src/index.ts
|
|
7
|
-
import { defineCommand as
|
|
13
|
+
import { defineCommand as defineCommand10, runMain } from "citty";
|
|
8
14
|
|
|
9
|
-
// src/commands/
|
|
15
|
+
// src/commands/audit.ts
|
|
10
16
|
import { defineCommand } from "citty";
|
|
11
17
|
import pc from "picocolors";
|
|
18
|
+
import { createSpinner } from "nanospinner";
|
|
19
|
+
|
|
20
|
+
// src/core/lockfile.ts
|
|
21
|
+
import fs from "fs";
|
|
22
|
+
import path from "path";
|
|
23
|
+
import os from "os";
|
|
24
|
+
var LOCKFILE_NAME = "mcpman.lock";
|
|
25
|
+
function findLockfile() {
|
|
26
|
+
let dir = process.cwd();
|
|
27
|
+
while (true) {
|
|
28
|
+
const candidate = path.join(dir, LOCKFILE_NAME);
|
|
29
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
30
|
+
const parent = path.dirname(dir);
|
|
31
|
+
if (parent === dir) break;
|
|
32
|
+
dir = parent;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function getGlobalLockfilePath() {
|
|
37
|
+
return path.join(os.homedir(), ".mcpman", LOCKFILE_NAME);
|
|
38
|
+
}
|
|
39
|
+
function resolveLockfilePath() {
|
|
40
|
+
return findLockfile() ?? getGlobalLockfilePath();
|
|
41
|
+
}
|
|
42
|
+
function readLockfile(filePath) {
|
|
43
|
+
const target = filePath ?? resolveLockfilePath();
|
|
44
|
+
if (!fs.existsSync(target)) {
|
|
45
|
+
return { lockfileVersion: 1, servers: {} };
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const raw = fs.readFileSync(target, "utf-8");
|
|
49
|
+
return JSON.parse(raw);
|
|
50
|
+
} catch {
|
|
51
|
+
return { lockfileVersion: 1, servers: {} };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function serialize(data) {
|
|
55
|
+
const sorted = {
|
|
56
|
+
lockfileVersion: data.lockfileVersion,
|
|
57
|
+
servers: Object.fromEntries(
|
|
58
|
+
Object.entries(data.servers).sort(([a], [b]) => a.localeCompare(b))
|
|
59
|
+
)
|
|
60
|
+
};
|
|
61
|
+
return JSON.stringify(sorted, null, 2) + "\n";
|
|
62
|
+
}
|
|
63
|
+
function writeLockfile(data, filePath) {
|
|
64
|
+
const target = filePath ?? resolveLockfilePath();
|
|
65
|
+
const dir = path.dirname(target);
|
|
66
|
+
if (!fs.existsSync(dir)) {
|
|
67
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
const tmp = `${target}.tmp`;
|
|
70
|
+
fs.writeFileSync(tmp, serialize(data), "utf-8");
|
|
71
|
+
fs.renameSync(tmp, target);
|
|
72
|
+
}
|
|
73
|
+
function addEntry(name, entry, filePath) {
|
|
74
|
+
const data = readLockfile(filePath);
|
|
75
|
+
data.servers[name] = entry;
|
|
76
|
+
writeLockfile(data, filePath);
|
|
77
|
+
}
|
|
78
|
+
function createEmptyLockfile(filePath) {
|
|
79
|
+
writeLockfile({ lockfileVersion: 1, servers: {} }, filePath);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/core/security-scanner.ts
|
|
83
|
+
import fs2 from "fs";
|
|
84
|
+
import os2 from "os";
|
|
85
|
+
import path2 from "path";
|
|
86
|
+
var CACHE_PATH = path2.join(os2.homedir(), ".mcpman", ".audit-cache.json");
|
|
87
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
88
|
+
function readCache() {
|
|
89
|
+
try {
|
|
90
|
+
if (!fs2.existsSync(CACHE_PATH)) return {};
|
|
91
|
+
return JSON.parse(fs2.readFileSync(CACHE_PATH, "utf-8"));
|
|
92
|
+
} catch {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function writeCache(cache) {
|
|
97
|
+
const dir = path2.dirname(CACHE_PATH);
|
|
98
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
99
|
+
const tmp = `${CACHE_PATH}.tmp`;
|
|
100
|
+
fs2.writeFileSync(tmp, JSON.stringify(cache, null, 2), "utf-8");
|
|
101
|
+
fs2.renameSync(tmp, CACHE_PATH);
|
|
102
|
+
}
|
|
103
|
+
function getCachedReport(name, version) {
|
|
104
|
+
const cache = readCache();
|
|
105
|
+
const key = `${name}@${version}`;
|
|
106
|
+
const entry = cache[key];
|
|
107
|
+
if (!entry) return null;
|
|
108
|
+
if (Date.now() - entry.timestamp > CACHE_TTL_MS) return null;
|
|
109
|
+
return entry.report;
|
|
110
|
+
}
|
|
111
|
+
function cacheReport(name, version, report) {
|
|
112
|
+
const cache = readCache();
|
|
113
|
+
cache[`${name}@${version}`] = { report, timestamp: Date.now() };
|
|
114
|
+
writeCache(cache);
|
|
115
|
+
}
|
|
116
|
+
async function fetchNpmMetadata(packageName) {
|
|
117
|
+
const timeout = 1e4;
|
|
118
|
+
const signal = AbortSignal.timeout(timeout);
|
|
119
|
+
try {
|
|
120
|
+
const [regRes, dlRes] = await Promise.all([
|
|
121
|
+
fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, { signal }),
|
|
122
|
+
fetch(`https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(packageName)}`, {
|
|
123
|
+
signal: AbortSignal.timeout(timeout)
|
|
124
|
+
})
|
|
125
|
+
]);
|
|
126
|
+
if (!regRes.ok) return null;
|
|
127
|
+
const reg = await regRes.json();
|
|
128
|
+
const time = reg["time"] ?? {};
|
|
129
|
+
const created = time["created"] ? new Date(time["created"]) : null;
|
|
130
|
+
const modified = time["modified"] ? new Date(time["modified"]) : null;
|
|
131
|
+
const packageAge = created ? Math.floor((Date.now() - created.getTime()) / 864e5) : 0;
|
|
132
|
+
const maintainers = Array.isArray(reg["maintainers"]) ? reg["maintainers"] : [];
|
|
133
|
+
const latestVersion = reg["dist-tags"]?.["latest"] ?? "";
|
|
134
|
+
const versionData = reg["versions"]?.[latestVersion];
|
|
135
|
+
const deprecated = typeof versionData?.["deprecated"] === "string";
|
|
136
|
+
let weeklyDownloads = 0;
|
|
137
|
+
if (dlRes.ok) {
|
|
138
|
+
const dl = await dlRes.json();
|
|
139
|
+
weeklyDownloads = typeof dl["downloads"] === "number" ? dl["downloads"] : 0;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
weeklyDownloads,
|
|
143
|
+
lastPublish: modified?.toISOString() ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
144
|
+
packageAge,
|
|
145
|
+
maintainerCount: maintainers.length,
|
|
146
|
+
deprecated
|
|
147
|
+
};
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function fetchVulnerabilities(packageName, version) {
|
|
153
|
+
try {
|
|
154
|
+
const res = await fetch("https://api.osv.dev/v1/query", {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: { "Content-Type": "application/json" },
|
|
157
|
+
body: JSON.stringify({ package: { name: packageName, ecosystem: "npm" }, version }),
|
|
158
|
+
signal: AbortSignal.timeout(1e4)
|
|
159
|
+
});
|
|
160
|
+
if (!res.ok) return [];
|
|
161
|
+
const data = await res.json();
|
|
162
|
+
const vulns = Array.isArray(data["vulns"]) ? data["vulns"] : [];
|
|
163
|
+
return vulns.map((v) => {
|
|
164
|
+
const severity = v["database_specific"]?.["severity"];
|
|
165
|
+
const sev = typeof severity === "string" ? severity.toLowerCase() : "moderate";
|
|
166
|
+
const refs = Array.isArray(v["references"]) ? v["references"] : [];
|
|
167
|
+
return {
|
|
168
|
+
severity: ["low", "moderate", "high", "critical"].includes(sev) ? sev : "moderate",
|
|
169
|
+
title: typeof v["summary"] === "string" ? v["summary"] : typeof v["id"] === "string" ? v["id"] : "Unknown vulnerability",
|
|
170
|
+
url: typeof refs[0]?.["url"] === "string" ? refs[0]["url"] : void 0
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
} catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function scanServer(name, entry) {
|
|
178
|
+
if (entry.source !== "npm") {
|
|
179
|
+
return {
|
|
180
|
+
server: name,
|
|
181
|
+
source: entry.source,
|
|
182
|
+
score: null,
|
|
183
|
+
riskLevel: "UNKNOWN",
|
|
184
|
+
vulnerabilities: [],
|
|
185
|
+
metadata: null
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const cached = getCachedReport(name, entry.version);
|
|
189
|
+
if (cached) return cached;
|
|
190
|
+
const [metadata, vulnerabilities] = await Promise.all([
|
|
191
|
+
fetchNpmMetadata(name),
|
|
192
|
+
fetchVulnerabilities(name, entry.version)
|
|
193
|
+
]);
|
|
194
|
+
const { computeTrustScore } = await import("./trust-scorer-LYC6KZCD.js");
|
|
195
|
+
const { score, riskLevel } = computeTrustScore(metadata, vulnerabilities);
|
|
196
|
+
const report = {
|
|
197
|
+
server: name,
|
|
198
|
+
source: "npm",
|
|
199
|
+
score,
|
|
200
|
+
riskLevel,
|
|
201
|
+
vulnerabilities,
|
|
202
|
+
metadata
|
|
203
|
+
};
|
|
204
|
+
cacheReport(name, entry.version, report);
|
|
205
|
+
return report;
|
|
206
|
+
}
|
|
207
|
+
async function scanAllServers(servers, concurrency = 3) {
|
|
208
|
+
const entries = Object.entries(servers);
|
|
209
|
+
const results = [];
|
|
210
|
+
const executing = /* @__PURE__ */ new Set();
|
|
211
|
+
for (const [name, entry] of entries) {
|
|
212
|
+
const p8 = scanServer(name, entry).then((r) => {
|
|
213
|
+
results.push(r);
|
|
214
|
+
executing.delete(p8);
|
|
215
|
+
});
|
|
216
|
+
executing.add(p8);
|
|
217
|
+
if (executing.size >= concurrency) await Promise.race(executing);
|
|
218
|
+
}
|
|
219
|
+
await Promise.all(executing);
|
|
220
|
+
return results;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/commands/audit.ts
|
|
224
|
+
function colorRisk(level, score) {
|
|
225
|
+
const label = score !== null ? `${score}/100 (${level})` : level;
|
|
226
|
+
if (level === "LOW") return pc.green(label);
|
|
227
|
+
if (level === "MEDIUM") return pc.yellow(label);
|
|
228
|
+
if (level === "HIGH") return pc.red(label);
|
|
229
|
+
if (level === "CRITICAL") return pc.bold(pc.red(label));
|
|
230
|
+
return pc.dim(label);
|
|
231
|
+
}
|
|
232
|
+
function daysAgo(isoDate) {
|
|
233
|
+
const days = Math.floor((Date.now() - new Date(isoDate).getTime()) / 864e5);
|
|
234
|
+
if (days === 0) return "today";
|
|
235
|
+
if (days === 1) return "1 day ago";
|
|
236
|
+
return `${days} days ago`;
|
|
237
|
+
}
|
|
238
|
+
function countVulns(vulns) {
|
|
239
|
+
const c = { critical: 0, high: 0, moderate: 0, low: 0 };
|
|
240
|
+
for (const v of vulns) c[v.severity]++;
|
|
241
|
+
if (vulns.length === 0) return pc.green("none");
|
|
242
|
+
const parts = [];
|
|
243
|
+
if (c.critical) parts.push(pc.bold(pc.red(`${c.critical} critical`)));
|
|
244
|
+
if (c.high) parts.push(pc.red(`${c.high} high`));
|
|
245
|
+
if (c.moderate) parts.push(pc.yellow(`${c.moderate} moderate`));
|
|
246
|
+
if (c.low) parts.push(pc.dim(`${c.low} low`));
|
|
247
|
+
return parts.join(", ");
|
|
248
|
+
}
|
|
249
|
+
function printReport(report) {
|
|
250
|
+
const riskColored = colorRisk(report.riskLevel, report.score);
|
|
251
|
+
const icon = report.riskLevel === "LOW" ? pc.green("\u25CF") : report.riskLevel === "MEDIUM" ? pc.yellow("\u25CF") : report.riskLevel === "UNKNOWN" ? pc.dim("\u25CB") : pc.red("\u25CF");
|
|
252
|
+
console.log(` ${icon} ${pc.bold(report.server)} Score: ${riskColored}`);
|
|
253
|
+
if (report.source !== "npm") {
|
|
254
|
+
console.log(` ${pc.dim("Non-npm source \u2014 security data unavailable")}`);
|
|
255
|
+
console.log();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (report.metadata) {
|
|
259
|
+
const { weeklyDownloads, packageAge, lastPublish, maintainerCount, deprecated } = report.metadata;
|
|
260
|
+
const dlStr = weeklyDownloads.toLocaleString();
|
|
261
|
+
console.log(
|
|
262
|
+
` ${pc.dim("Downloads:")} ${dlStr}/week ${pc.dim("|")} ${pc.dim("Age:")} ${packageAge}d ${pc.dim("|")} ${pc.dim("Last publish:")} ${daysAgo(lastPublish)} ${pc.dim("|")} ${pc.dim("Maintainers:")} ${maintainerCount}` + (deprecated ? pc.red(" [DEPRECATED]") : "")
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
console.log(` ${pc.dim("Vulnerabilities:")} ${countVulns(report.vulnerabilities)}`);
|
|
266
|
+
if (report.vulnerabilities.length > 0) {
|
|
267
|
+
for (const v of report.vulnerabilities) {
|
|
268
|
+
const sevColor = v.severity === "critical" || v.severity === "high" ? pc.red : pc.yellow;
|
|
269
|
+
const url = v.url ? pc.dim(` ${v.url}`) : "";
|
|
270
|
+
console.log(` ${sevColor("\u25B8")} [${v.severity}] ${v.title}${url}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
console.log();
|
|
274
|
+
}
|
|
275
|
+
var audit_default = defineCommand({
|
|
276
|
+
meta: {
|
|
277
|
+
name: "audit",
|
|
278
|
+
description: "Scan installed MCP servers for security vulnerabilities and trust scores"
|
|
279
|
+
},
|
|
280
|
+
args: {
|
|
281
|
+
server: {
|
|
282
|
+
type: "positional",
|
|
283
|
+
description: "Specific server to audit (omit to audit all)",
|
|
284
|
+
required: false
|
|
285
|
+
},
|
|
286
|
+
json: {
|
|
287
|
+
type: "boolean",
|
|
288
|
+
description: "Output results as JSON",
|
|
289
|
+
default: false
|
|
290
|
+
},
|
|
291
|
+
fix: {
|
|
292
|
+
type: "boolean",
|
|
293
|
+
description: "Show available fix versions for vulnerable packages",
|
|
294
|
+
default: false
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
async run({ args }) {
|
|
298
|
+
const lockfile = readLockfile();
|
|
299
|
+
const { servers } = lockfile;
|
|
300
|
+
if (Object.keys(servers).length === 0) {
|
|
301
|
+
console.log(pc.dim("\n No MCP servers installed. Run mcpman install <server> to get started.\n"));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const targets = {};
|
|
305
|
+
if (args.server) {
|
|
306
|
+
if (!servers[args.server]) {
|
|
307
|
+
console.error(pc.red(`
|
|
308
|
+
Server "${args.server}" not found in lockfile.
|
|
309
|
+
`));
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
targets[args.server] = servers[args.server];
|
|
313
|
+
} else {
|
|
314
|
+
Object.assign(targets, servers);
|
|
315
|
+
}
|
|
316
|
+
const spinner5 = createSpinner(`Scanning ${Object.keys(targets).length} server(s)...`).start();
|
|
317
|
+
let reports;
|
|
318
|
+
try {
|
|
319
|
+
reports = args.server ? [await scanServer(args.server, targets[args.server])] : await scanAllServers(targets);
|
|
320
|
+
} catch (err) {
|
|
321
|
+
spinner5.error({ text: "Scan failed" });
|
|
322
|
+
console.error(pc.red(String(err)));
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
spinner5.success({ text: `Scanned ${reports.length} server(s)` });
|
|
326
|
+
if (args.json) {
|
|
327
|
+
console.log(JSON.stringify(reports, null, 2));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
console.log(pc.bold("\n mcpman audit\n"));
|
|
331
|
+
console.log(pc.dim(" " + "\u2500".repeat(60)));
|
|
332
|
+
for (const report of reports) {
|
|
333
|
+
printReport(report);
|
|
334
|
+
}
|
|
335
|
+
console.log(pc.dim(" " + "\u2500".repeat(60)));
|
|
336
|
+
const withIssues = reports.filter(
|
|
337
|
+
(r) => r.riskLevel !== "LOW" && r.riskLevel !== "UNKNOWN"
|
|
338
|
+
);
|
|
339
|
+
const npmReports = reports.filter((r) => r.source === "npm");
|
|
340
|
+
const parts = [];
|
|
341
|
+
parts.push(`${reports.length} server(s) scanned`);
|
|
342
|
+
if (npmReports.length < reports.length) {
|
|
343
|
+
parts.push(pc.dim(`${reports.length - npmReports.length} non-npm (unverified)`));
|
|
344
|
+
}
|
|
345
|
+
if (withIssues.length > 0) {
|
|
346
|
+
parts.push(pc.yellow(`${withIssues.length} with issues`));
|
|
347
|
+
} else {
|
|
348
|
+
parts.push(pc.green("all clear"));
|
|
349
|
+
}
|
|
350
|
+
console.log(`
|
|
351
|
+
Summary: ${parts.join(" | ")}
|
|
352
|
+
`);
|
|
353
|
+
if (args.fix) {
|
|
354
|
+
const withVulns = reports.filter((r) => r.vulnerabilities.length > 0);
|
|
355
|
+
if (withVulns.length > 0) {
|
|
356
|
+
console.log(pc.bold(" Fix suggestions:"));
|
|
357
|
+
for (const r of withVulns) {
|
|
358
|
+
console.log(` ${pc.cyan("\u2192")} Run ${pc.cyan(`mcpman install ${r.server}@latest`)} to update`);
|
|
359
|
+
}
|
|
360
|
+
console.log();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// src/commands/doctor.ts
|
|
367
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
368
|
+
import pc2 from "picocolors";
|
|
12
369
|
|
|
13
370
|
// src/core/server-inventory.ts
|
|
14
371
|
async function getInstalledServers(clientFilter) {
|
|
@@ -272,12 +629,12 @@ async function quickHealthProbe(config, timeoutMs = 3e3) {
|
|
|
272
629
|
|
|
273
630
|
// src/commands/doctor.ts
|
|
274
631
|
var CHECK_ICON = {
|
|
275
|
-
pass:
|
|
276
|
-
fail:
|
|
277
|
-
skip:
|
|
278
|
-
warn:
|
|
632
|
+
pass: pc2.green("\u2713"),
|
|
633
|
+
fail: pc2.red("\u2717"),
|
|
634
|
+
skip: pc2.dim("-"),
|
|
635
|
+
warn: pc2.yellow("\u26A0")
|
|
279
636
|
};
|
|
280
|
-
var doctor_default =
|
|
637
|
+
var doctor_default = defineCommand2({
|
|
281
638
|
meta: {
|
|
282
639
|
name: "doctor",
|
|
283
640
|
description: "Check MCP server health and configuration"
|
|
@@ -290,10 +647,10 @@ var doctor_default = defineCommand({
|
|
|
290
647
|
}
|
|
291
648
|
},
|
|
292
649
|
async run({ args }) {
|
|
293
|
-
console.log(
|
|
650
|
+
console.log(pc2.bold("\n mcpman doctor\n"));
|
|
294
651
|
const servers = await getInstalledServers();
|
|
295
652
|
if (servers.length === 0) {
|
|
296
|
-
console.log(
|
|
653
|
+
console.log(pc2.dim(" No MCP servers installed. Run mcpman install <server> to get started."));
|
|
297
654
|
return;
|
|
298
655
|
}
|
|
299
656
|
const tasks = servers.map((s) => () => checkServerHealth(s.name, s.config));
|
|
@@ -305,14 +662,14 @@ var doctor_default = defineCommand({
|
|
|
305
662
|
if (result.status === "healthy") passed++;
|
|
306
663
|
else failed++;
|
|
307
664
|
}
|
|
308
|
-
console.log(
|
|
665
|
+
console.log(pc2.dim(" " + "\u2500".repeat(50)));
|
|
309
666
|
const parts = [];
|
|
310
|
-
if (passed > 0) parts.push(
|
|
311
|
-
if (failed > 0) parts.push(
|
|
667
|
+
if (passed > 0) parts.push(pc2.green(`${passed} healthy`));
|
|
668
|
+
if (failed > 0) parts.push(pc2.red(`${failed} unhealthy`));
|
|
312
669
|
console.log(` Summary: ${parts.join(", ")}`);
|
|
313
670
|
if (failed > 0) {
|
|
314
671
|
if (!args.fix) {
|
|
315
|
-
console.log(
|
|
672
|
+
console.log(pc2.dim(` Run ${pc2.cyan("mcpman doctor --fix")} for fix suggestions.
|
|
316
673
|
`));
|
|
317
674
|
}
|
|
318
675
|
process.exit(1);
|
|
@@ -321,13 +678,13 @@ var doctor_default = defineCommand({
|
|
|
321
678
|
}
|
|
322
679
|
});
|
|
323
680
|
function printServerResult(result, showFix) {
|
|
324
|
-
const icon = result.status === "healthy" ?
|
|
325
|
-
console.log(` ${icon} ${
|
|
681
|
+
const icon = result.status === "healthy" ? pc2.green("\u25CF") : pc2.red("\u25CF");
|
|
682
|
+
console.log(` ${icon} ${pc2.bold(result.serverName)}`);
|
|
326
683
|
for (const check of result.checks) {
|
|
327
684
|
const checkIcon = check.skipped ? CHECK_ICON.skip : check.passed ? CHECK_ICON.pass : CHECK_ICON.fail;
|
|
328
685
|
console.log(` ${checkIcon} ${check.name}: ${check.message}`);
|
|
329
686
|
if (showFix && !check.passed && !check.skipped && check.fix) {
|
|
330
|
-
console.log(` ${
|
|
687
|
+
console.log(` ${pc2.yellow("\u2192")} Fix: ${pc2.cyan(check.fix)}`);
|
|
331
688
|
}
|
|
332
689
|
}
|
|
333
690
|
console.log();
|
|
@@ -336,11 +693,11 @@ async function runParallel(tasks, concurrency) {
|
|
|
336
693
|
const results = [];
|
|
337
694
|
const executing = /* @__PURE__ */ new Set();
|
|
338
695
|
for (const task of tasks) {
|
|
339
|
-
const
|
|
696
|
+
const p8 = task().then((r) => {
|
|
340
697
|
results.push(r);
|
|
341
|
-
executing.delete(
|
|
698
|
+
executing.delete(p8);
|
|
342
699
|
});
|
|
343
|
-
executing.add(
|
|
700
|
+
executing.add(p8);
|
|
344
701
|
if (executing.size >= concurrency) {
|
|
345
702
|
await Promise.race(executing);
|
|
346
703
|
}
|
|
@@ -350,71 +707,9 @@ async function runParallel(tasks, concurrency) {
|
|
|
350
707
|
}
|
|
351
708
|
|
|
352
709
|
// src/commands/init.ts
|
|
353
|
-
import { defineCommand as
|
|
710
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
354
711
|
import * as p from "@clack/prompts";
|
|
355
|
-
import
|
|
356
|
-
|
|
357
|
-
// src/core/lockfile.ts
|
|
358
|
-
import fs from "fs";
|
|
359
|
-
import path from "path";
|
|
360
|
-
import os from "os";
|
|
361
|
-
var LOCKFILE_NAME = "mcpman.lock";
|
|
362
|
-
function findLockfile() {
|
|
363
|
-
let dir = process.cwd();
|
|
364
|
-
while (true) {
|
|
365
|
-
const candidate = path.join(dir, LOCKFILE_NAME);
|
|
366
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
367
|
-
const parent = path.dirname(dir);
|
|
368
|
-
if (parent === dir) break;
|
|
369
|
-
dir = parent;
|
|
370
|
-
}
|
|
371
|
-
return null;
|
|
372
|
-
}
|
|
373
|
-
function getGlobalLockfilePath() {
|
|
374
|
-
return path.join(os.homedir(), ".mcpman", LOCKFILE_NAME);
|
|
375
|
-
}
|
|
376
|
-
function resolveLockfilePath() {
|
|
377
|
-
return findLockfile() ?? getGlobalLockfilePath();
|
|
378
|
-
}
|
|
379
|
-
function readLockfile(filePath) {
|
|
380
|
-
const target = filePath ?? resolveLockfilePath();
|
|
381
|
-
if (!fs.existsSync(target)) {
|
|
382
|
-
return { lockfileVersion: 1, servers: {} };
|
|
383
|
-
}
|
|
384
|
-
try {
|
|
385
|
-
const raw = fs.readFileSync(target, "utf-8");
|
|
386
|
-
return JSON.parse(raw);
|
|
387
|
-
} catch {
|
|
388
|
-
return { lockfileVersion: 1, servers: {} };
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
function serialize(data) {
|
|
392
|
-
const sorted = {
|
|
393
|
-
lockfileVersion: data.lockfileVersion,
|
|
394
|
-
servers: Object.fromEntries(
|
|
395
|
-
Object.entries(data.servers).sort(([a], [b]) => a.localeCompare(b))
|
|
396
|
-
)
|
|
397
|
-
};
|
|
398
|
-
return JSON.stringify(sorted, null, 2) + "\n";
|
|
399
|
-
}
|
|
400
|
-
function writeLockfile(data, filePath) {
|
|
401
|
-
const target = filePath ?? resolveLockfilePath();
|
|
402
|
-
const dir = path.dirname(target);
|
|
403
|
-
if (!fs.existsSync(dir)) {
|
|
404
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
405
|
-
}
|
|
406
|
-
const tmp = `${target}.tmp`;
|
|
407
|
-
fs.writeFileSync(tmp, serialize(data), "utf-8");
|
|
408
|
-
fs.renameSync(tmp, target);
|
|
409
|
-
}
|
|
410
|
-
function addEntry(name, entry, filePath) {
|
|
411
|
-
const data = readLockfile(filePath);
|
|
412
|
-
data.servers[name] = entry;
|
|
413
|
-
writeLockfile(data, filePath);
|
|
414
|
-
}
|
|
415
|
-
function createEmptyLockfile(filePath) {
|
|
416
|
-
writeLockfile({ lockfileVersion: 1, servers: {} }, filePath);
|
|
417
|
-
}
|
|
712
|
+
import path3 from "path";
|
|
418
713
|
|
|
419
714
|
// src/core/registry.ts
|
|
420
715
|
import { createHash } from "crypto";
|
|
@@ -525,22 +820,34 @@ async function resolveFromGitHub(githubUrl) {
|
|
|
525
820
|
}
|
|
526
821
|
|
|
527
822
|
// src/commands/init.ts
|
|
528
|
-
var init_default =
|
|
823
|
+
var init_default = defineCommand3({
|
|
529
824
|
meta: {
|
|
530
825
|
name: "init",
|
|
531
826
|
description: "Initialize mcpman.lock in the current project"
|
|
532
827
|
},
|
|
533
|
-
args: {
|
|
534
|
-
|
|
828
|
+
args: {
|
|
829
|
+
yes: {
|
|
830
|
+
type: "boolean",
|
|
831
|
+
alias: "y",
|
|
832
|
+
description: "Auto-import all servers without prompting",
|
|
833
|
+
default: false
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
async run({ args }) {
|
|
837
|
+
const nonInteractive = args.yes || !process.stdout.isTTY;
|
|
535
838
|
p.intro("mcpman init");
|
|
536
|
-
const targetPath =
|
|
839
|
+
const targetPath = path3.join(process.cwd(), LOCKFILE_NAME);
|
|
537
840
|
const existing = findLockfile();
|
|
538
841
|
if (existing) {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
p.
|
|
543
|
-
|
|
842
|
+
if (nonInteractive) {
|
|
843
|
+
p.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
|
|
844
|
+
} else {
|
|
845
|
+
p.log.warn(`Lockfile already exists: ${existing}`);
|
|
846
|
+
const overwrite = await p.confirm({ message: "Overwrite?" });
|
|
847
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
848
|
+
p.outro("Cancelled.");
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
544
851
|
}
|
|
545
852
|
}
|
|
546
853
|
let clients = [];
|
|
@@ -566,20 +873,26 @@ var init_default = defineCommand2({
|
|
|
566
873
|
p.outro(`Created ${LOCKFILE_NAME} \u2014 add it to version control!`);
|
|
567
874
|
return;
|
|
568
875
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
876
|
+
let selected;
|
|
877
|
+
if (nonInteractive) {
|
|
878
|
+
selected = clientServers.map((cs) => cs.client.type);
|
|
879
|
+
p.log.info(`Non-interactive mode: importing all ${clientServers.length} client(s).`);
|
|
880
|
+
} else {
|
|
881
|
+
const options = clientServers.map((cs) => ({
|
|
882
|
+
value: cs.client.type,
|
|
883
|
+
label: `${cs.client.displayName} (${Object.keys(cs.servers).length} servers)`
|
|
884
|
+
}));
|
|
885
|
+
const toImport = await p.multiselect({
|
|
886
|
+
message: "Import existing servers into lockfile?",
|
|
887
|
+
options,
|
|
888
|
+
required: false
|
|
889
|
+
});
|
|
890
|
+
if (p.isCancel(toImport)) {
|
|
891
|
+
p.outro(`Created empty ${LOCKFILE_NAME}`);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
selected = toImport;
|
|
581
895
|
}
|
|
582
|
-
const selected = toImport;
|
|
583
896
|
let importCount = 0;
|
|
584
897
|
for (const cs of clientServers) {
|
|
585
898
|
if (!selected.includes(cs.client.type)) continue;
|
|
@@ -608,7 +921,7 @@ var init_default = defineCommand2({
|
|
|
608
921
|
});
|
|
609
922
|
|
|
610
923
|
// src/commands/install.ts
|
|
611
|
-
import { defineCommand as
|
|
924
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
612
925
|
|
|
613
926
|
// src/core/installer.ts
|
|
614
927
|
import * as p2 from "@clack/prompts";
|
|
@@ -658,17 +971,17 @@ async function loadClients() {
|
|
|
658
971
|
}
|
|
659
972
|
async function installServer(input, options = {}) {
|
|
660
973
|
p2.intro("mcpman install");
|
|
661
|
-
const
|
|
662
|
-
|
|
974
|
+
const spinner5 = p2.spinner();
|
|
975
|
+
spinner5.start("Resolving server...");
|
|
663
976
|
let metadata;
|
|
664
977
|
try {
|
|
665
978
|
metadata = await resolveServer(input);
|
|
666
979
|
} catch (err) {
|
|
667
|
-
|
|
980
|
+
spinner5.stop("Resolution failed");
|
|
668
981
|
p2.log.error(err instanceof Error ? err.message : String(err));
|
|
669
982
|
process.exit(1);
|
|
670
983
|
}
|
|
671
|
-
|
|
984
|
+
spinner5.stop(`Found: ${metadata.name}@${metadata.version}`);
|
|
672
985
|
const clients = await loadClients();
|
|
673
986
|
if (clients.length === 0) {
|
|
674
987
|
p2.log.warn("No supported AI clients detected on this machine.");
|
|
@@ -722,18 +1035,18 @@ async function installServer(input, options = {}) {
|
|
|
722
1035
|
args: metadata.args,
|
|
723
1036
|
...Object.keys(collectedEnv).length > 0 ? { env: collectedEnv } : {}
|
|
724
1037
|
};
|
|
725
|
-
|
|
1038
|
+
spinner5.start("Writing config...");
|
|
726
1039
|
const clientTypes = [];
|
|
727
1040
|
for (const client of selectedClients) {
|
|
728
1041
|
try {
|
|
729
1042
|
await client.addServer(metadata.name, entry);
|
|
730
1043
|
clientTypes.push(client.type);
|
|
731
1044
|
} catch (err) {
|
|
732
|
-
|
|
1045
|
+
spinner5.stop("Partial failure");
|
|
733
1046
|
p2.log.warn(`Failed to write to ${client.displayName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
734
1047
|
}
|
|
735
1048
|
}
|
|
736
|
-
|
|
1049
|
+
spinner5.stop("Config written");
|
|
737
1050
|
const source = detectSource(input);
|
|
738
1051
|
const integrity = computeIntegrity(metadata.resolved);
|
|
739
1052
|
addEntry(metadata.name, {
|
|
@@ -754,7 +1067,7 @@ async function installServer(input, options = {}) {
|
|
|
754
1067
|
}
|
|
755
1068
|
|
|
756
1069
|
// src/utils/logger.ts
|
|
757
|
-
import
|
|
1070
|
+
import pc3 from "picocolors";
|
|
758
1071
|
var noColor = process.env.NO_COLOR !== void 0 || process.argv.includes("--no-color");
|
|
759
1072
|
var isVerbose = process.argv.includes("--verbose");
|
|
760
1073
|
var isJson = process.argv.includes("--json");
|
|
@@ -763,11 +1076,11 @@ function colorize(fn, text2) {
|
|
|
763
1076
|
}
|
|
764
1077
|
function info(message) {
|
|
765
1078
|
if (isJson) return;
|
|
766
|
-
console.log(`${colorize(
|
|
1079
|
+
console.log(`${colorize(pc3.cyan, "i")} ${message}`);
|
|
767
1080
|
}
|
|
768
1081
|
function error(message) {
|
|
769
1082
|
if (isJson) return;
|
|
770
|
-
console.error(`${colorize(
|
|
1083
|
+
console.error(`${colorize(pc3.red, "\u2717")} ${message}`);
|
|
771
1084
|
}
|
|
772
1085
|
function json(data) {
|
|
773
1086
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -775,7 +1088,7 @@ function json(data) {
|
|
|
775
1088
|
|
|
776
1089
|
// src/commands/install.ts
|
|
777
1090
|
import * as p3 from "@clack/prompts";
|
|
778
|
-
var install_default =
|
|
1091
|
+
var install_default = defineCommand4({
|
|
779
1092
|
meta: {
|
|
780
1093
|
name: "install",
|
|
781
1094
|
description: "Install an MCP server into one or more AI clients"
|
|
@@ -837,14 +1150,14 @@ async function restoreFromLockfile() {
|
|
|
837
1150
|
}
|
|
838
1151
|
|
|
839
1152
|
// src/commands/list.ts
|
|
840
|
-
import { defineCommand as
|
|
841
|
-
import
|
|
1153
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
1154
|
+
import pc4 from "picocolors";
|
|
842
1155
|
var STATUS_ICON = {
|
|
843
|
-
healthy:
|
|
844
|
-
unhealthy:
|
|
845
|
-
unknown:
|
|
1156
|
+
healthy: pc4.green("\u25CF"),
|
|
1157
|
+
unhealthy: pc4.red("\u25CF"),
|
|
1158
|
+
unknown: pc4.dim("\u25CB")
|
|
846
1159
|
};
|
|
847
|
-
var list_default =
|
|
1160
|
+
var list_default = defineCommand5({
|
|
848
1161
|
meta: {
|
|
849
1162
|
name: "list",
|
|
850
1163
|
description: "List installed MCP servers"
|
|
@@ -864,13 +1177,13 @@ var list_default = defineCommand4({
|
|
|
864
1177
|
const servers = await getInstalledServers(args.client);
|
|
865
1178
|
if (servers.length === 0) {
|
|
866
1179
|
const filter = args.client ? ` for client "${args.client}"` : "";
|
|
867
|
-
console.log(
|
|
1180
|
+
console.log(pc4.dim(`No MCP servers installed${filter}. Run ${pc4.cyan("mcpman install <server>")} to get started.`));
|
|
868
1181
|
return;
|
|
869
1182
|
}
|
|
870
1183
|
const withStatus = await Promise.all(
|
|
871
1184
|
servers.map(async (s) => ({
|
|
872
1185
|
...s,
|
|
873
|
-
status: await quickHealthProbe(s.config,
|
|
1186
|
+
status: await quickHealthProbe(s.config, 5e3)
|
|
874
1187
|
}))
|
|
875
1188
|
);
|
|
876
1189
|
if (args.json) {
|
|
@@ -888,8 +1201,8 @@ var list_default = defineCommand4({
|
|
|
888
1201
|
const nameWidth = Math.max(4, ...withStatus.map((s) => s.name.length));
|
|
889
1202
|
const clientsWidth = Math.max(7, ...withStatus.map((s) => formatClients(s.clients).length));
|
|
890
1203
|
const header = ` ${pad("NAME", nameWidth)} ${pad("CLIENT(S)", clientsWidth)} ${pad("COMMAND", 20)} STATUS`;
|
|
891
|
-
console.log(
|
|
892
|
-
console.log(
|
|
1204
|
+
console.log(pc4.dim(header));
|
|
1205
|
+
console.log(pc4.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientsWidth)} ${"-".repeat(20)} ------`));
|
|
893
1206
|
for (const s of withStatus) {
|
|
894
1207
|
const icon = STATUS_ICON[s.status];
|
|
895
1208
|
const clientsStr = formatClients(s.clients);
|
|
@@ -897,7 +1210,7 @@ var list_default = defineCommand4({
|
|
|
897
1210
|
console.log(` ${pad(s.name, nameWidth)} ${pad(clientsStr, clientsWidth)} ${pad(cmdStr, 20)} ${icon} ${s.status}`);
|
|
898
1211
|
}
|
|
899
1212
|
const clientSet = new Set(withStatus.flatMap((s) => s.clients));
|
|
900
|
-
console.log(
|
|
1213
|
+
console.log(pc4.dim(`
|
|
901
1214
|
${withStatus.length} server${withStatus.length !== 1 ? "s" : ""} \xB7 ${clientSet.size} client${clientSet.size !== 1 ? "s" : ""}`));
|
|
902
1215
|
}
|
|
903
1216
|
});
|
|
@@ -918,9 +1231,9 @@ function formatClients(clients) {
|
|
|
918
1231
|
}
|
|
919
1232
|
|
|
920
1233
|
// src/commands/remove.ts
|
|
921
|
-
import { defineCommand as
|
|
1234
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
922
1235
|
import * as p4 from "@clack/prompts";
|
|
923
|
-
import
|
|
1236
|
+
import pc5 from "picocolors";
|
|
924
1237
|
var CLIENT_DISPLAY2 = {
|
|
925
1238
|
"claude-desktop": "Claude",
|
|
926
1239
|
cursor: "Cursor",
|
|
@@ -930,7 +1243,7 @@ var CLIENT_DISPLAY2 = {
|
|
|
930
1243
|
function clientDisplayName(type) {
|
|
931
1244
|
return CLIENT_DISPLAY2[type] ?? type;
|
|
932
1245
|
}
|
|
933
|
-
var remove_default =
|
|
1246
|
+
var remove_default = defineCommand6({
|
|
934
1247
|
meta: {
|
|
935
1248
|
name: "remove",
|
|
936
1249
|
description: "Remove an MCP server from one or more AI clients"
|
|
@@ -957,7 +1270,7 @@ var remove_default = defineCommand5({
|
|
|
957
1270
|
}
|
|
958
1271
|
},
|
|
959
1272
|
async run({ args }) {
|
|
960
|
-
p4.intro(
|
|
1273
|
+
p4.intro(pc5.bold("mcpman remove"));
|
|
961
1274
|
const serverName = args.server;
|
|
962
1275
|
const servers = await getInstalledServers();
|
|
963
1276
|
const match = servers.find((s) => s.name === serverName);
|
|
@@ -965,7 +1278,7 @@ var remove_default = defineCommand5({
|
|
|
965
1278
|
p4.log.warn(`Server "${serverName}" is not installed.`);
|
|
966
1279
|
const similar = servers.filter((s) => s.name.includes(serverName) || serverName.includes(s.name));
|
|
967
1280
|
if (similar.length > 0) {
|
|
968
|
-
p4.log.info(`Did you mean: ${similar.map((s) =>
|
|
1281
|
+
p4.log.info(`Did you mean: ${similar.map((s) => pc5.cyan(s.name)).join(", ")}?`);
|
|
969
1282
|
}
|
|
970
1283
|
p4.outro("Nothing to remove.");
|
|
971
1284
|
return;
|
|
@@ -1000,7 +1313,7 @@ var remove_default = defineCommand5({
|
|
|
1000
1313
|
if (!args.yes) {
|
|
1001
1314
|
const clientNames = targetClients.map(clientDisplayName).join(", ");
|
|
1002
1315
|
const confirmed = await p4.confirm({
|
|
1003
|
-
message: `Remove ${
|
|
1316
|
+
message: `Remove ${pc5.cyan(serverName)} from ${pc5.yellow(clientNames)}?`
|
|
1004
1317
|
});
|
|
1005
1318
|
if (p4.isCancel(confirmed) || !confirmed) {
|
|
1006
1319
|
p4.outro("Cancelled.");
|
|
@@ -1025,16 +1338,687 @@ var remove_default = defineCommand5({
|
|
|
1025
1338
|
}
|
|
1026
1339
|
if (errors.length > 0) {
|
|
1027
1340
|
for (const e of errors) p4.log.error(e);
|
|
1028
|
-
p4.outro(
|
|
1341
|
+
p4.outro(pc5.red("Completed with errors."));
|
|
1342
|
+
process.exit(1);
|
|
1343
|
+
}
|
|
1344
|
+
p4.outro(pc5.green(`Removed "${serverName}" successfully.`));
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
// src/commands/secrets.ts
|
|
1349
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
1350
|
+
import pc6 from "picocolors";
|
|
1351
|
+
import * as p5 from "@clack/prompts";
|
|
1352
|
+
function maskValue(value) {
|
|
1353
|
+
if (value.length <= 8) return "***";
|
|
1354
|
+
return `${value.slice(0, 4)}***${value.slice(-3)}`;
|
|
1355
|
+
}
|
|
1356
|
+
function parseKeyValue(input) {
|
|
1357
|
+
const idx = input.indexOf("=");
|
|
1358
|
+
if (idx <= 0) return null;
|
|
1359
|
+
return { key: input.slice(0, idx), value: input.slice(idx + 1) };
|
|
1360
|
+
}
|
|
1361
|
+
var setCommand = defineCommand7({
|
|
1362
|
+
meta: { name: "set", description: "Store an encrypted secret for a server" },
|
|
1363
|
+
args: {
|
|
1364
|
+
server: {
|
|
1365
|
+
type: "positional",
|
|
1366
|
+
description: "Server name (e.g. @modelcontextprotocol/server-github)",
|
|
1367
|
+
required: true
|
|
1368
|
+
},
|
|
1369
|
+
keyvalue: {
|
|
1370
|
+
type: "positional",
|
|
1371
|
+
description: "KEY=VALUE pair to store",
|
|
1372
|
+
required: true
|
|
1373
|
+
}
|
|
1374
|
+
},
|
|
1375
|
+
async run({ args }) {
|
|
1376
|
+
const parsed = parseKeyValue(args.keyvalue);
|
|
1377
|
+
if (!parsed) {
|
|
1378
|
+
console.error(pc6.red("\u2717") + " Invalid format. Expected KEY=VALUE");
|
|
1379
|
+
process.exit(1);
|
|
1380
|
+
}
|
|
1381
|
+
p5.intro(pc6.cyan("mcpman secrets set"));
|
|
1382
|
+
const isNew = listSecrets(args.server).length === 0 || !listSecrets(args.server)[0]?.keys.includes(parsed.key);
|
|
1383
|
+
const vaultPath = (await import("./vault-service-UTZAV6N6.js")).getVaultPath();
|
|
1384
|
+
const vaultExists = (await import("fs")).existsSync(vaultPath);
|
|
1385
|
+
const password = await getMasterPassword(!vaultExists && isNew);
|
|
1386
|
+
const spin = p5.spinner();
|
|
1387
|
+
spin.start("Encrypting secret...");
|
|
1388
|
+
try {
|
|
1389
|
+
setSecret(args.server, parsed.key, parsed.value, password);
|
|
1390
|
+
spin.stop(
|
|
1391
|
+
`${pc6.green("\u2713")} Stored ${pc6.bold(parsed.key)} for ${pc6.cyan(args.server)}`
|
|
1392
|
+
);
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
spin.stop(pc6.red("\u2717") + " Failed to store secret");
|
|
1395
|
+
console.error(pc6.dim(String(err)));
|
|
1396
|
+
process.exit(1);
|
|
1397
|
+
}
|
|
1398
|
+
p5.outro(pc6.dim("Secret encrypted and saved to vault."));
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
var listCommand = defineCommand7({
|
|
1402
|
+
meta: { name: "list", description: "List secret keys stored in the vault" },
|
|
1403
|
+
args: {
|
|
1404
|
+
server: {
|
|
1405
|
+
type: "positional",
|
|
1406
|
+
description: "Filter by server name (optional)",
|
|
1407
|
+
required: false
|
|
1408
|
+
}
|
|
1409
|
+
},
|
|
1410
|
+
async run({ args }) {
|
|
1411
|
+
const results = listSecrets(args.server || void 0);
|
|
1412
|
+
if (results.length === 0) {
|
|
1413
|
+
const filter = args.server ? ` for ${pc6.cyan(args.server)}` : "";
|
|
1414
|
+
console.log(pc6.dim(`No secrets stored${filter}.`));
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
console.log("");
|
|
1418
|
+
for (const { server, keys } of results) {
|
|
1419
|
+
console.log(pc6.bold(pc6.cyan(server)));
|
|
1420
|
+
for (const key of keys) {
|
|
1421
|
+
console.log(` ${pc6.green("\u25CF")} ${pc6.bold(key)} ${pc6.dim(maskValue("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"))}`);
|
|
1422
|
+
}
|
|
1423
|
+
console.log("");
|
|
1424
|
+
}
|
|
1425
|
+
const total = results.reduce((n, r) => n + r.keys.length, 0);
|
|
1426
|
+
console.log(pc6.dim(` ${total} secret${total !== 1 ? "s" : ""} in ${results.length} server${results.length !== 1 ? "s" : ""}`));
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
var removeCommand = defineCommand7({
|
|
1430
|
+
meta: { name: "remove", description: "Delete a secret from the vault" },
|
|
1431
|
+
args: {
|
|
1432
|
+
server: {
|
|
1433
|
+
type: "positional",
|
|
1434
|
+
description: "Server name",
|
|
1435
|
+
required: true
|
|
1436
|
+
},
|
|
1437
|
+
key: {
|
|
1438
|
+
type: "positional",
|
|
1439
|
+
description: "Secret key to remove",
|
|
1440
|
+
required: true
|
|
1441
|
+
}
|
|
1442
|
+
},
|
|
1443
|
+
async run({ args }) {
|
|
1444
|
+
const confirmed = await p5.confirm({
|
|
1445
|
+
message: `Remove ${pc6.bold(args.key)} from ${pc6.cyan(args.server)}?`,
|
|
1446
|
+
initialValue: false
|
|
1447
|
+
});
|
|
1448
|
+
if (p5.isCancel(confirmed) || !confirmed) {
|
|
1449
|
+
p5.cancel("Cancelled.");
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
try {
|
|
1453
|
+
removeSecret(args.server, args.key);
|
|
1454
|
+
console.log(`${pc6.green("\u2713")} Removed ${pc6.bold(args.key)} from ${pc6.cyan(args.server)}`);
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
console.error(pc6.red("\u2717") + " Failed to remove secret");
|
|
1457
|
+
console.error(pc6.dim(String(err)));
|
|
1458
|
+
process.exit(1);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
var secrets_default = defineCommand7({
|
|
1463
|
+
meta: {
|
|
1464
|
+
name: "secrets",
|
|
1465
|
+
description: "Manage encrypted secrets for MCP servers"
|
|
1466
|
+
},
|
|
1467
|
+
subCommands: {
|
|
1468
|
+
set: setCommand,
|
|
1469
|
+
list: listCommand,
|
|
1470
|
+
remove: removeCommand
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
// src/commands/sync.ts
|
|
1475
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
1476
|
+
import * as p6 from "@clack/prompts";
|
|
1477
|
+
import pc7 from "picocolors";
|
|
1478
|
+
|
|
1479
|
+
// src/core/config-diff.ts
|
|
1480
|
+
function reconstructServerEntry(lockEntry) {
|
|
1481
|
+
const entry = {
|
|
1482
|
+
command: lockEntry.command
|
|
1483
|
+
};
|
|
1484
|
+
if (lockEntry.args && lockEntry.args.length > 0) {
|
|
1485
|
+
entry.args = lockEntry.args;
|
|
1486
|
+
}
|
|
1487
|
+
if (lockEntry.envVars && lockEntry.envVars.length > 0) {
|
|
1488
|
+
entry.env = Object.fromEntries(lockEntry.envVars.map((k) => [k, ""]));
|
|
1489
|
+
}
|
|
1490
|
+
return entry;
|
|
1491
|
+
}
|
|
1492
|
+
function computeDiff(lockfile, clientConfigs) {
|
|
1493
|
+
const actions = [];
|
|
1494
|
+
for (const [server, lockEntry] of Object.entries(lockfile.servers)) {
|
|
1495
|
+
for (const client of lockEntry.clients) {
|
|
1496
|
+
const config = clientConfigs.get(client);
|
|
1497
|
+
if (!config) continue;
|
|
1498
|
+
if (server in config.servers) {
|
|
1499
|
+
actions.push({ server, client, action: "ok" });
|
|
1500
|
+
} else {
|
|
1501
|
+
actions.push({
|
|
1502
|
+
server,
|
|
1503
|
+
client,
|
|
1504
|
+
action: "add",
|
|
1505
|
+
entry: reconstructServerEntry(lockEntry)
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
for (const [client, config] of clientConfigs) {
|
|
1511
|
+
for (const server of Object.keys(config.servers)) {
|
|
1512
|
+
if (!(server in lockfile.servers)) {
|
|
1513
|
+
actions.push({ server, client, action: "extra" });
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return actions;
|
|
1518
|
+
}
|
|
1519
|
+
function computeDiffFromClient(sourceClient, clientConfigs) {
|
|
1520
|
+
const actions = [];
|
|
1521
|
+
const sourceConfig = clientConfigs.get(sourceClient);
|
|
1522
|
+
if (!sourceConfig) return [];
|
|
1523
|
+
for (const [client, config] of clientConfigs) {
|
|
1524
|
+
if (client === sourceClient) continue;
|
|
1525
|
+
for (const [server, entry] of Object.entries(sourceConfig.servers)) {
|
|
1526
|
+
if (server in config.servers) {
|
|
1527
|
+
actions.push({ server, client, action: "ok" });
|
|
1528
|
+
} else {
|
|
1529
|
+
actions.push({ server, client, action: "add", entry });
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
for (const server of Object.keys(config.servers)) {
|
|
1533
|
+
if (!(server in sourceConfig.servers)) {
|
|
1534
|
+
actions.push({ server, client, action: "extra" });
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return actions;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// src/core/sync-engine.ts
|
|
1542
|
+
async function applySyncActions(actions, clients) {
|
|
1543
|
+
const result = { applied: 0, failed: 0, errors: [] };
|
|
1544
|
+
const addActions = actions.filter((a) => a.action === "add" && a.entry);
|
|
1545
|
+
for (const action of addActions) {
|
|
1546
|
+
const handler = clients.get(action.client);
|
|
1547
|
+
if (!handler || !action.entry) {
|
|
1548
|
+
result.failed++;
|
|
1549
|
+
result.errors.push({
|
|
1550
|
+
server: action.server,
|
|
1551
|
+
client: action.client,
|
|
1552
|
+
error: "No handler available for client"
|
|
1553
|
+
});
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
try {
|
|
1557
|
+
await handler.addServer(action.server, action.entry);
|
|
1558
|
+
result.applied++;
|
|
1559
|
+
} catch (err) {
|
|
1560
|
+
result.failed++;
|
|
1561
|
+
result.errors.push({
|
|
1562
|
+
server: action.server,
|
|
1563
|
+
client: action.client,
|
|
1564
|
+
error: String(err)
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
return result;
|
|
1569
|
+
}
|
|
1570
|
+
async function getClientConfigs() {
|
|
1571
|
+
const configs = /* @__PURE__ */ new Map();
|
|
1572
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
1573
|
+
let installedClients;
|
|
1574
|
+
try {
|
|
1575
|
+
installedClients = await getInstalledClients();
|
|
1576
|
+
} catch {
|
|
1577
|
+
return { configs, handlers };
|
|
1578
|
+
}
|
|
1579
|
+
await Promise.all(
|
|
1580
|
+
installedClients.map(async (handler) => {
|
|
1581
|
+
try {
|
|
1582
|
+
const config = await handler.readConfig();
|
|
1583
|
+
configs.set(handler.type, config);
|
|
1584
|
+
handlers.set(handler.type, handler);
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
console.warn(`[mcpman] Warning: could not read config for ${handler.displayName}: ${String(err)}`);
|
|
1587
|
+
}
|
|
1588
|
+
})
|
|
1589
|
+
);
|
|
1590
|
+
return { configs, handlers };
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// src/commands/sync.ts
|
|
1594
|
+
var VALID_CLIENTS = ["claude-desktop", "cursor", "vscode", "windsurf"];
|
|
1595
|
+
var CLIENT_DISPLAY3 = {
|
|
1596
|
+
"claude-desktop": "Claude Desktop",
|
|
1597
|
+
cursor: "Cursor",
|
|
1598
|
+
vscode: "VS Code",
|
|
1599
|
+
windsurf: "Windsurf"
|
|
1600
|
+
};
|
|
1601
|
+
var sync_default = defineCommand8({
|
|
1602
|
+
meta: {
|
|
1603
|
+
name: "sync",
|
|
1604
|
+
description: "Sync MCP server configs across all detected AI clients"
|
|
1605
|
+
},
|
|
1606
|
+
args: {
|
|
1607
|
+
"dry-run": {
|
|
1608
|
+
type: "boolean",
|
|
1609
|
+
description: "Preview changes without applying them",
|
|
1610
|
+
default: false
|
|
1611
|
+
},
|
|
1612
|
+
source: {
|
|
1613
|
+
type: "string",
|
|
1614
|
+
description: "Use a specific client as source of truth (claude-desktop, cursor, vscode, windsurf)"
|
|
1615
|
+
},
|
|
1616
|
+
yes: {
|
|
1617
|
+
type: "boolean",
|
|
1618
|
+
description: "Skip confirmation prompt",
|
|
1619
|
+
default: false
|
|
1620
|
+
}
|
|
1621
|
+
},
|
|
1622
|
+
async run({ args }) {
|
|
1623
|
+
p6.intro(`${pc7.cyan("mcpman sync")}`);
|
|
1624
|
+
const sourceClient = args.source;
|
|
1625
|
+
if (sourceClient && !VALID_CLIENTS.includes(sourceClient)) {
|
|
1626
|
+
p6.log.error(`Invalid --source "${sourceClient}". Must be one of: ${VALID_CLIENTS.join(", ")}`);
|
|
1627
|
+
process.exit(1);
|
|
1628
|
+
}
|
|
1629
|
+
const spinner5 = p6.spinner();
|
|
1630
|
+
spinner5.start("Detecting clients and reading configs...");
|
|
1631
|
+
const { configs, handlers } = await getClientConfigs();
|
|
1632
|
+
spinner5.stop(`Found ${configs.size} client(s)`);
|
|
1633
|
+
if (configs.size === 0) {
|
|
1634
|
+
p6.log.warn("No AI clients detected. Install Claude Desktop, Cursor, VS Code, or Windsurf first.");
|
|
1635
|
+
process.exit(0);
|
|
1636
|
+
}
|
|
1637
|
+
let actions;
|
|
1638
|
+
if (sourceClient) {
|
|
1639
|
+
if (!configs.has(sourceClient)) {
|
|
1640
|
+
p6.log.error(`Source client "${sourceClient}" is not detected or its config is unreadable.`);
|
|
1641
|
+
process.exit(1);
|
|
1642
|
+
}
|
|
1643
|
+
p6.log.info(`Using ${CLIENT_DISPLAY3[sourceClient]} as source of truth`);
|
|
1644
|
+
actions = computeDiffFromClient(sourceClient, configs);
|
|
1645
|
+
} else {
|
|
1646
|
+
const lockfile = readLockfile();
|
|
1647
|
+
actions = computeDiff(lockfile, configs);
|
|
1648
|
+
}
|
|
1649
|
+
printDiffTable(actions);
|
|
1650
|
+
const addCount = actions.filter((a) => a.action === "add").length;
|
|
1651
|
+
const extraCount = actions.filter((a) => a.action === "extra").length;
|
|
1652
|
+
if (addCount === 0 && extraCount === 0) {
|
|
1653
|
+
p6.outro(pc7.green("All clients are in sync."));
|
|
1654
|
+
process.exit(0);
|
|
1655
|
+
}
|
|
1656
|
+
const parts = [];
|
|
1657
|
+
if (addCount > 0) parts.push(pc7.green(`${addCount} to add`));
|
|
1658
|
+
if (extraCount > 0) parts.push(pc7.yellow(`${extraCount} extra (informational)`));
|
|
1659
|
+
p6.log.info(parts.join(" \xB7 "));
|
|
1660
|
+
if (args["dry-run"]) {
|
|
1661
|
+
p6.outro(pc7.dim("Dry run \u2014 no changes applied."));
|
|
1662
|
+
process.exit(1);
|
|
1663
|
+
}
|
|
1664
|
+
if (addCount === 0) {
|
|
1665
|
+
p6.outro(pc7.dim("No additions needed. Extra servers left untouched."));
|
|
1666
|
+
process.exit(1);
|
|
1667
|
+
}
|
|
1668
|
+
if (!args.yes) {
|
|
1669
|
+
const confirmed = await p6.confirm({
|
|
1670
|
+
message: `Apply ${addCount} addition(s) to client configs?`,
|
|
1671
|
+
initialValue: true
|
|
1672
|
+
});
|
|
1673
|
+
if (p6.isCancel(confirmed) || !confirmed) {
|
|
1674
|
+
p6.outro(pc7.dim("Cancelled \u2014 no changes applied."));
|
|
1675
|
+
process.exit(0);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
spinner5.start("Applying sync changes...");
|
|
1679
|
+
const result = await applySyncActions(actions, handlers);
|
|
1680
|
+
spinner5.stop("Done");
|
|
1681
|
+
if (result.applied > 0) {
|
|
1682
|
+
p6.log.success(`Added ${result.applied} server(s) to client configs.`);
|
|
1683
|
+
}
|
|
1684
|
+
if (result.failed > 0) {
|
|
1685
|
+
for (const e of result.errors) {
|
|
1686
|
+
p6.log.error(`Failed to add "${e.server}" to ${e.client}: ${e.error}`);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
p6.outro(result.failed === 0 ? pc7.green("Sync complete.") : pc7.yellow("Sync complete with errors."));
|
|
1690
|
+
process.exit(result.failed > 0 ? 1 : 0);
|
|
1691
|
+
}
|
|
1692
|
+
});
|
|
1693
|
+
function printDiffTable(actions) {
|
|
1694
|
+
if (actions.length === 0) {
|
|
1695
|
+
p6.log.info("No actions to display.");
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
const nameWidth = Math.max(6, ...actions.map((a) => a.server.length));
|
|
1699
|
+
const clientWidth = Math.max(6, ...actions.map((a) => CLIENT_DISPLAY3[a.client]?.length ?? a.client.length));
|
|
1700
|
+
const header = ` ${pad2("SERVER", nameWidth)} ${pad2("CLIENT", clientWidth)} STATUS`;
|
|
1701
|
+
console.log(pc7.dim(header));
|
|
1702
|
+
console.log(pc7.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientWidth)} ------`));
|
|
1703
|
+
for (const action of actions) {
|
|
1704
|
+
const clientDisplay = CLIENT_DISPLAY3[action.client] ?? action.client;
|
|
1705
|
+
const [icon, statusText] = formatAction(action.action);
|
|
1706
|
+
console.log(` ${pad2(action.server, nameWidth)} ${pad2(clientDisplay, clientWidth)} ${icon} ${statusText}`);
|
|
1707
|
+
}
|
|
1708
|
+
console.log("");
|
|
1709
|
+
}
|
|
1710
|
+
function formatAction(action) {
|
|
1711
|
+
switch (action) {
|
|
1712
|
+
case "add":
|
|
1713
|
+
return [pc7.green("+"), pc7.green("missing \u2014 will add")];
|
|
1714
|
+
case "extra":
|
|
1715
|
+
return [pc7.yellow("?"), pc7.yellow("extra (not in lockfile)")];
|
|
1716
|
+
case "ok":
|
|
1717
|
+
return [pc7.dim("\xB7"), pc7.dim("in sync")];
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
function pad2(s, width) {
|
|
1721
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// src/commands/update.ts
|
|
1725
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
1726
|
+
import * as p7 from "@clack/prompts";
|
|
1727
|
+
import pc9 from "picocolors";
|
|
1728
|
+
|
|
1729
|
+
// src/core/version-checker.ts
|
|
1730
|
+
function compareVersions(a, b) {
|
|
1731
|
+
const aParts = a.replace(/^v/, "").split(".").map(Number);
|
|
1732
|
+
const bParts = b.replace(/^v/, "").split(".").map(Number);
|
|
1733
|
+
const len = Math.max(aParts.length, bParts.length);
|
|
1734
|
+
for (let i = 0; i < len; i++) {
|
|
1735
|
+
const aN = aParts[i] ?? 0;
|
|
1736
|
+
const bN = bParts[i] ?? 0;
|
|
1737
|
+
if (Number.isNaN(aN) || Number.isNaN(bN)) return 0;
|
|
1738
|
+
if (aN < bN) return -1;
|
|
1739
|
+
if (aN > bN) return 1;
|
|
1740
|
+
}
|
|
1741
|
+
return 0;
|
|
1742
|
+
}
|
|
1743
|
+
function detectUpdateType(current, latest) {
|
|
1744
|
+
const cParts = current.replace(/^v/, "").split(".").map(Number);
|
|
1745
|
+
const lParts = latest.replace(/^v/, "").split(".").map(Number);
|
|
1746
|
+
if ((lParts[0] ?? 0) > (cParts[0] ?? 0)) return "major";
|
|
1747
|
+
if ((lParts[1] ?? 0) > (cParts[1] ?? 0)) return "minor";
|
|
1748
|
+
return "patch";
|
|
1749
|
+
}
|
|
1750
|
+
async function fetchNpmLatest(packageName) {
|
|
1751
|
+
try {
|
|
1752
|
+
const res = await fetch(
|
|
1753
|
+
`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
|
|
1754
|
+
{
|
|
1755
|
+
headers: { Accept: "application/json" },
|
|
1756
|
+
signal: AbortSignal.timeout(8e3)
|
|
1757
|
+
}
|
|
1758
|
+
);
|
|
1759
|
+
if (!res.ok) return null;
|
|
1760
|
+
const data = await res.json();
|
|
1761
|
+
return typeof data.version === "string" ? data.version : null;
|
|
1762
|
+
} catch {
|
|
1763
|
+
return null;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
async function fetchSmitheryLatest(name) {
|
|
1767
|
+
try {
|
|
1768
|
+
const res = await fetch(
|
|
1769
|
+
`https://registry.smithery.ai/servers/${encodeURIComponent(name)}`,
|
|
1770
|
+
{
|
|
1771
|
+
headers: { Accept: "application/json" },
|
|
1772
|
+
signal: AbortSignal.timeout(8e3)
|
|
1773
|
+
}
|
|
1774
|
+
);
|
|
1775
|
+
if (!res.ok) return null;
|
|
1776
|
+
const data = await res.json();
|
|
1777
|
+
return typeof data.version === "string" ? data.version : null;
|
|
1778
|
+
} catch {
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
async function fetchGithubLatest(resolved) {
|
|
1783
|
+
const match = resolved.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
1784
|
+
if (!match) return null;
|
|
1785
|
+
const [, owner, repo] = match;
|
|
1786
|
+
try {
|
|
1787
|
+
const res = await fetch(
|
|
1788
|
+
`https://api.github.com/repos/${owner}/${repo}/releases/latest`,
|
|
1789
|
+
{
|
|
1790
|
+
headers: { Accept: "application/json" },
|
|
1791
|
+
signal: AbortSignal.timeout(8e3)
|
|
1792
|
+
}
|
|
1793
|
+
);
|
|
1794
|
+
if (!res.ok) return null;
|
|
1795
|
+
const data = await res.json();
|
|
1796
|
+
return typeof data.tag_name === "string" ? data.tag_name.replace(/^v/, "") : null;
|
|
1797
|
+
} catch {
|
|
1798
|
+
return null;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
async function checkVersion(name, lockEntry) {
|
|
1802
|
+
const current = lockEntry.version;
|
|
1803
|
+
let latest = null;
|
|
1804
|
+
if (lockEntry.source === "npm") {
|
|
1805
|
+
latest = await fetchNpmLatest(name);
|
|
1806
|
+
} else if (lockEntry.source === "smithery") {
|
|
1807
|
+
latest = await fetchSmitheryLatest(name);
|
|
1808
|
+
} else if (lockEntry.source === "github") {
|
|
1809
|
+
latest = await fetchGithubLatest(lockEntry.resolved);
|
|
1810
|
+
}
|
|
1811
|
+
if (!latest || latest === current) {
|
|
1812
|
+
return {
|
|
1813
|
+
server: name,
|
|
1814
|
+
source: lockEntry.source,
|
|
1815
|
+
currentVersion: current,
|
|
1816
|
+
latestVersion: latest ?? current,
|
|
1817
|
+
hasUpdate: false
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
const hasUpdate = compareVersions(current, latest) === -1;
|
|
1821
|
+
return {
|
|
1822
|
+
server: name,
|
|
1823
|
+
source: lockEntry.source,
|
|
1824
|
+
currentVersion: current,
|
|
1825
|
+
latestVersion: latest,
|
|
1826
|
+
hasUpdate,
|
|
1827
|
+
updateType: hasUpdate ? detectUpdateType(current, latest) : void 0
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
async function checkAllVersions(lockfile) {
|
|
1831
|
+
const entries = Object.entries(lockfile.servers);
|
|
1832
|
+
if (entries.length === 0) return [];
|
|
1833
|
+
const results = [];
|
|
1834
|
+
const executing = /* @__PURE__ */ new Set();
|
|
1835
|
+
for (const [name, entry] of entries) {
|
|
1836
|
+
const p8 = checkVersion(name, entry).then((r) => {
|
|
1837
|
+
results.push(r);
|
|
1838
|
+
executing.delete(p8);
|
|
1839
|
+
});
|
|
1840
|
+
executing.add(p8);
|
|
1841
|
+
if (executing.size >= 5) {
|
|
1842
|
+
await Promise.race(executing);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
await Promise.all(executing);
|
|
1846
|
+
return results;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// src/core/update-notifier.ts
|
|
1850
|
+
import fs3 from "fs";
|
|
1851
|
+
import path4 from "path";
|
|
1852
|
+
import os3 from "os";
|
|
1853
|
+
import pc8 from "picocolors";
|
|
1854
|
+
var CACHE_FILE = path4.join(os3.homedir(), ".mcpman", ".update-check");
|
|
1855
|
+
var TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1856
|
+
function writeUpdateCache(data) {
|
|
1857
|
+
try {
|
|
1858
|
+
const dir = path4.dirname(CACHE_FILE);
|
|
1859
|
+
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
1860
|
+
const tmp = `${CACHE_FILE}.tmp`;
|
|
1861
|
+
fs3.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
1862
|
+
fs3.renameSync(tmp, CACHE_FILE);
|
|
1863
|
+
} catch {
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// src/commands/update.ts
|
|
1868
|
+
async function loadClients2() {
|
|
1869
|
+
try {
|
|
1870
|
+
const mod = await import("./client-detector-SUIJSIYM.js");
|
|
1871
|
+
return mod.getInstalledClients();
|
|
1872
|
+
} catch {
|
|
1873
|
+
return [];
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
function printTable(updates) {
|
|
1877
|
+
const NAME_W = 28;
|
|
1878
|
+
const VER_W = 10;
|
|
1879
|
+
const header = [
|
|
1880
|
+
"NAME".padEnd(NAME_W),
|
|
1881
|
+
"CURRENT".padEnd(VER_W),
|
|
1882
|
+
"LATEST".padEnd(VER_W),
|
|
1883
|
+
"STATUS"
|
|
1884
|
+
].join(" ");
|
|
1885
|
+
console.log(pc9.bold(`
|
|
1886
|
+
${header}`));
|
|
1887
|
+
console.log(pc9.dim(` ${"\u2500".repeat(NAME_W + VER_W * 2 + 20)}`));
|
|
1888
|
+
for (const u of updates) {
|
|
1889
|
+
const nameCol = u.server.slice(0, NAME_W).padEnd(NAME_W);
|
|
1890
|
+
const curCol = u.currentVersion.padEnd(VER_W);
|
|
1891
|
+
const latCol = u.latestVersion.padEnd(VER_W);
|
|
1892
|
+
const statusCol = u.hasUpdate ? pc9.yellow(`Update available${u.updateType ? ` [${u.updateType}]` : ""}`) : pc9.green("Up to date");
|
|
1893
|
+
console.log(` ${nameCol} ${curCol} ${latCol} ${statusCol}`);
|
|
1894
|
+
}
|
|
1895
|
+
console.log();
|
|
1896
|
+
}
|
|
1897
|
+
var update_default = defineCommand9({
|
|
1898
|
+
meta: {
|
|
1899
|
+
name: "update",
|
|
1900
|
+
description: "Check for and apply updates to installed MCP servers"
|
|
1901
|
+
},
|
|
1902
|
+
args: {
|
|
1903
|
+
server: {
|
|
1904
|
+
type: "positional",
|
|
1905
|
+
description: "Server name to update (omit to update all)",
|
|
1906
|
+
required: false
|
|
1907
|
+
},
|
|
1908
|
+
check: {
|
|
1909
|
+
type: "boolean",
|
|
1910
|
+
description: "Check only \u2014 do not apply updates",
|
|
1911
|
+
default: false
|
|
1912
|
+
},
|
|
1913
|
+
yes: {
|
|
1914
|
+
type: "boolean",
|
|
1915
|
+
description: "Skip confirmation prompt",
|
|
1916
|
+
default: false
|
|
1917
|
+
},
|
|
1918
|
+
json: {
|
|
1919
|
+
type: "boolean",
|
|
1920
|
+
description: "Output results as JSON",
|
|
1921
|
+
default: false
|
|
1922
|
+
}
|
|
1923
|
+
},
|
|
1924
|
+
async run({ args }) {
|
|
1925
|
+
const lockfile = readLockfile();
|
|
1926
|
+
const servers = lockfile.servers;
|
|
1927
|
+
const targetEntries = args.server ? Object.entries(servers).filter(([name]) => name === args.server) : Object.entries(servers);
|
|
1928
|
+
if (targetEntries.length === 0) {
|
|
1929
|
+
if (args.server) {
|
|
1930
|
+
console.error(`Server '${args.server}' not found in lockfile.`);
|
|
1931
|
+
} else {
|
|
1932
|
+
console.log("No servers installed. Run mcpman install <server> first.");
|
|
1933
|
+
}
|
|
1934
|
+
process.exit(1);
|
|
1935
|
+
}
|
|
1936
|
+
const spinner5 = p7.spinner();
|
|
1937
|
+
spinner5.start("Checking versions...");
|
|
1938
|
+
let updates;
|
|
1939
|
+
try {
|
|
1940
|
+
const partialLock = {
|
|
1941
|
+
lockfileVersion: 1,
|
|
1942
|
+
servers: Object.fromEntries(targetEntries)
|
|
1943
|
+
};
|
|
1944
|
+
updates = await checkAllVersions(partialLock);
|
|
1945
|
+
} catch (err) {
|
|
1946
|
+
spinner5.stop("Version check failed");
|
|
1947
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1029
1948
|
process.exit(1);
|
|
1030
1949
|
}
|
|
1031
|
-
|
|
1950
|
+
spinner5.stop(`Checked ${updates.length} server(s)`);
|
|
1951
|
+
if (args.json) {
|
|
1952
|
+
console.log(JSON.stringify(updates, null, 2));
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
printTable(updates);
|
|
1956
|
+
const outdated = updates.filter((u) => u.hasUpdate);
|
|
1957
|
+
if (outdated.length === 0) {
|
|
1958
|
+
console.log(pc9.green(" All servers are up to date."));
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
if (args.check) {
|
|
1962
|
+
console.log(pc9.yellow(` ${outdated.length} update(s) available. Run mcpman update to apply.`));
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
if (!args.yes) {
|
|
1966
|
+
const confirmed = await p7.confirm({
|
|
1967
|
+
message: `Apply ${outdated.length} update(s)?`,
|
|
1968
|
+
initialValue: true
|
|
1969
|
+
});
|
|
1970
|
+
if (p7.isCancel(confirmed) || !confirmed) {
|
|
1971
|
+
p7.outro("Cancelled.");
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
const clients = await loadClients2();
|
|
1976
|
+
let successCount = 0;
|
|
1977
|
+
for (const update of outdated) {
|
|
1978
|
+
const lockEntry = servers[update.server];
|
|
1979
|
+
const input = lockEntry.source === "smithery" ? `smithery:${update.server}` : lockEntry.source === "github" ? lockEntry.resolved : update.server;
|
|
1980
|
+
const s = p7.spinner();
|
|
1981
|
+
s.start(`Updating ${update.server}...`);
|
|
1982
|
+
try {
|
|
1983
|
+
const metadata = await resolveServer(input);
|
|
1984
|
+
const integrity = computeIntegrity(metadata.resolved);
|
|
1985
|
+
addEntry(update.server, {
|
|
1986
|
+
...lockEntry,
|
|
1987
|
+
version: metadata.version,
|
|
1988
|
+
resolved: metadata.resolved,
|
|
1989
|
+
integrity,
|
|
1990
|
+
command: metadata.command,
|
|
1991
|
+
args: metadata.args,
|
|
1992
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1993
|
+
});
|
|
1994
|
+
const entryClients = clients.filter(
|
|
1995
|
+
(c) => lockEntry.clients.includes(c.type)
|
|
1996
|
+
);
|
|
1997
|
+
for (const client of entryClients) {
|
|
1998
|
+
try {
|
|
1999
|
+
await client.addServer(update.server, {
|
|
2000
|
+
command: metadata.command,
|
|
2001
|
+
args: metadata.args
|
|
2002
|
+
});
|
|
2003
|
+
} catch {
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
s.stop(`${pc9.green("\u2713")} ${update.server}: ${update.currentVersion} \u2192 ${metadata.version}`);
|
|
2007
|
+
successCount++;
|
|
2008
|
+
} catch (err) {
|
|
2009
|
+
s.stop(`${pc9.red("\u2717")} ${update.server}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
const freshLockfile = readLockfile(resolveLockfilePath());
|
|
2013
|
+
const freshUpdates = await checkAllVersions(freshLockfile);
|
|
2014
|
+
writeUpdateCache({ lastCheck: (/* @__PURE__ */ new Date()).toISOString(), updates: freshUpdates });
|
|
2015
|
+
p7.outro(`${successCount} of ${outdated.length} server(s) updated.`);
|
|
1032
2016
|
}
|
|
1033
2017
|
});
|
|
1034
2018
|
|
|
1035
2019
|
// src/utils/constants.ts
|
|
1036
2020
|
var APP_NAME = "mcpman";
|
|
1037
|
-
var APP_VERSION = "0.
|
|
2021
|
+
var APP_VERSION = "0.2.0";
|
|
1038
2022
|
var APP_DESCRIPTION = "The package manager for MCP servers";
|
|
1039
2023
|
|
|
1040
2024
|
// src/index.ts
|
|
@@ -1042,7 +2026,7 @@ process.on("SIGINT", () => {
|
|
|
1042
2026
|
console.log("\nAborted.");
|
|
1043
2027
|
process.exit(130);
|
|
1044
2028
|
});
|
|
1045
|
-
var main =
|
|
2029
|
+
var main = defineCommand10({
|
|
1046
2030
|
meta: {
|
|
1047
2031
|
name: APP_NAME,
|
|
1048
2032
|
version: APP_VERSION,
|
|
@@ -1053,7 +2037,11 @@ var main = defineCommand6({
|
|
|
1053
2037
|
list: list_default,
|
|
1054
2038
|
remove: remove_default,
|
|
1055
2039
|
doctor: doctor_default,
|
|
1056
|
-
init: init_default
|
|
2040
|
+
init: init_default,
|
|
2041
|
+
secrets: secrets_default,
|
|
2042
|
+
sync: sync_default,
|
|
2043
|
+
audit: audit_default,
|
|
2044
|
+
update: update_default
|
|
1057
2045
|
}
|
|
1058
2046
|
});
|
|
1059
2047
|
runMain(main);
|