mcpman 0.1.1 → 0.3.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 +68 -2
- package/dist/chunk-6X6Q6UZC.js +141 -0
- package/dist/index.cjs +1769 -356
- package/dist/index.js +1479 -308
- package/dist/trust-scorer-LYC6KZCD.js +77 -0
- package/dist/vault-service-UTZAV6N6.js +29 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,13 +2,777 @@
|
|
|
2
2
|
import {
|
|
3
3
|
getInstalledClients
|
|
4
4
|
} from "./chunk-QY22QTBR.js";
|
|
5
|
+
import {
|
|
6
|
+
getMasterPassword,
|
|
7
|
+
getSecretsForServer,
|
|
8
|
+
listSecrets,
|
|
9
|
+
removeSecret,
|
|
10
|
+
setSecret
|
|
11
|
+
} from "./chunk-6X6Q6UZC.js";
|
|
5
12
|
|
|
6
13
|
// src/index.ts
|
|
7
|
-
import { defineCommand as
|
|
14
|
+
import { defineCommand as defineCommand10, runMain } from "citty";
|
|
15
|
+
|
|
16
|
+
// src/commands/audit.ts
|
|
17
|
+
import { defineCommand } from "citty";
|
|
18
|
+
import * as p from "@clack/prompts";
|
|
19
|
+
import pc from "picocolors";
|
|
20
|
+
import { createSpinner } from "nanospinner";
|
|
21
|
+
|
|
22
|
+
// src/core/lockfile.ts
|
|
23
|
+
import fs from "fs";
|
|
24
|
+
import path from "path";
|
|
25
|
+
import os from "os";
|
|
26
|
+
var LOCKFILE_NAME = "mcpman.lock";
|
|
27
|
+
function findLockfile() {
|
|
28
|
+
let dir = process.cwd();
|
|
29
|
+
while (true) {
|
|
30
|
+
const candidate = path.join(dir, LOCKFILE_NAME);
|
|
31
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
32
|
+
const parent = path.dirname(dir);
|
|
33
|
+
if (parent === dir) break;
|
|
34
|
+
dir = parent;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function getGlobalLockfilePath() {
|
|
39
|
+
return path.join(os.homedir(), ".mcpman", LOCKFILE_NAME);
|
|
40
|
+
}
|
|
41
|
+
function resolveLockfilePath() {
|
|
42
|
+
return findLockfile() ?? getGlobalLockfilePath();
|
|
43
|
+
}
|
|
44
|
+
function readLockfile(filePath) {
|
|
45
|
+
const target = filePath ?? resolveLockfilePath();
|
|
46
|
+
if (!fs.existsSync(target)) {
|
|
47
|
+
return { lockfileVersion: 1, servers: {} };
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const raw = fs.readFileSync(target, "utf-8");
|
|
51
|
+
return JSON.parse(raw);
|
|
52
|
+
} catch {
|
|
53
|
+
return { lockfileVersion: 1, servers: {} };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function serialize(data) {
|
|
57
|
+
const sorted = {
|
|
58
|
+
lockfileVersion: data.lockfileVersion,
|
|
59
|
+
servers: Object.fromEntries(
|
|
60
|
+
Object.entries(data.servers).sort(([a], [b]) => a.localeCompare(b))
|
|
61
|
+
)
|
|
62
|
+
};
|
|
63
|
+
return JSON.stringify(sorted, null, 2) + "\n";
|
|
64
|
+
}
|
|
65
|
+
function writeLockfile(data, filePath) {
|
|
66
|
+
const target = filePath ?? resolveLockfilePath();
|
|
67
|
+
const dir = path.dirname(target);
|
|
68
|
+
if (!fs.existsSync(dir)) {
|
|
69
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
const tmp = `${target}.tmp`;
|
|
72
|
+
fs.writeFileSync(tmp, serialize(data), "utf-8");
|
|
73
|
+
fs.renameSync(tmp, target);
|
|
74
|
+
}
|
|
75
|
+
function addEntry(name, entry, filePath) {
|
|
76
|
+
const data = readLockfile(filePath);
|
|
77
|
+
data.servers[name] = entry;
|
|
78
|
+
writeLockfile(data, filePath);
|
|
79
|
+
}
|
|
80
|
+
function createEmptyLockfile(filePath) {
|
|
81
|
+
writeLockfile({ lockfileVersion: 1, servers: {} }, filePath);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/core/security-scanner.ts
|
|
85
|
+
import fs2 from "fs";
|
|
86
|
+
import os2 from "os";
|
|
87
|
+
import path2 from "path";
|
|
88
|
+
var CACHE_PATH = path2.join(os2.homedir(), ".mcpman", ".audit-cache.json");
|
|
89
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
90
|
+
function readCache() {
|
|
91
|
+
try {
|
|
92
|
+
if (!fs2.existsSync(CACHE_PATH)) return {};
|
|
93
|
+
return JSON.parse(fs2.readFileSync(CACHE_PATH, "utf-8"));
|
|
94
|
+
} catch {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function writeCache(cache) {
|
|
99
|
+
const dir = path2.dirname(CACHE_PATH);
|
|
100
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
101
|
+
const tmp = `${CACHE_PATH}.tmp`;
|
|
102
|
+
fs2.writeFileSync(tmp, JSON.stringify(cache, null, 2), "utf-8");
|
|
103
|
+
fs2.renameSync(tmp, CACHE_PATH);
|
|
104
|
+
}
|
|
105
|
+
function getCachedReport(name, version) {
|
|
106
|
+
const cache = readCache();
|
|
107
|
+
const key = `${name}@${version}`;
|
|
108
|
+
const entry = cache[key];
|
|
109
|
+
if (!entry) return null;
|
|
110
|
+
if (Date.now() - entry.timestamp > CACHE_TTL_MS) return null;
|
|
111
|
+
return entry.report;
|
|
112
|
+
}
|
|
113
|
+
function cacheReport(name, version, report) {
|
|
114
|
+
const cache = readCache();
|
|
115
|
+
cache[`${name}@${version}`] = { report, timestamp: Date.now() };
|
|
116
|
+
writeCache(cache);
|
|
117
|
+
}
|
|
118
|
+
async function fetchNpmMetadata(packageName) {
|
|
119
|
+
const timeout = 1e4;
|
|
120
|
+
const signal = AbortSignal.timeout(timeout);
|
|
121
|
+
try {
|
|
122
|
+
const [regRes, dlRes] = await Promise.all([
|
|
123
|
+
fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, { signal }),
|
|
124
|
+
fetch(`https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(packageName)}`, {
|
|
125
|
+
signal: AbortSignal.timeout(timeout)
|
|
126
|
+
})
|
|
127
|
+
]);
|
|
128
|
+
if (!regRes.ok) return null;
|
|
129
|
+
const reg = await regRes.json();
|
|
130
|
+
const time = reg["time"] ?? {};
|
|
131
|
+
const created = time["created"] ? new Date(time["created"]) : null;
|
|
132
|
+
const modified = time["modified"] ? new Date(time["modified"]) : null;
|
|
133
|
+
const packageAge = created ? Math.floor((Date.now() - created.getTime()) / 864e5) : 0;
|
|
134
|
+
const maintainers = Array.isArray(reg["maintainers"]) ? reg["maintainers"] : [];
|
|
135
|
+
const latestVersion = reg["dist-tags"]?.["latest"] ?? "";
|
|
136
|
+
const versionData = reg["versions"]?.[latestVersion];
|
|
137
|
+
const deprecated = typeof versionData?.["deprecated"] === "string";
|
|
138
|
+
let weeklyDownloads = 0;
|
|
139
|
+
if (dlRes.ok) {
|
|
140
|
+
const dl = await dlRes.json();
|
|
141
|
+
weeklyDownloads = typeof dl["downloads"] === "number" ? dl["downloads"] : 0;
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
weeklyDownloads,
|
|
145
|
+
lastPublish: modified?.toISOString() ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
146
|
+
packageAge,
|
|
147
|
+
maintainerCount: maintainers.length,
|
|
148
|
+
deprecated
|
|
149
|
+
};
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function fetchVulnerabilities(packageName, version) {
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetch("https://api.osv.dev/v1/query", {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: { "Content-Type": "application/json" },
|
|
159
|
+
body: JSON.stringify({ package: { name: packageName, ecosystem: "npm" }, version }),
|
|
160
|
+
signal: AbortSignal.timeout(1e4)
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok) return [];
|
|
163
|
+
const data = await res.json();
|
|
164
|
+
const vulns = Array.isArray(data["vulns"]) ? data["vulns"] : [];
|
|
165
|
+
return vulns.map((v) => {
|
|
166
|
+
const severity = v["database_specific"]?.["severity"];
|
|
167
|
+
const sev = typeof severity === "string" ? severity.toLowerCase() : "moderate";
|
|
168
|
+
const refs = Array.isArray(v["references"]) ? v["references"] : [];
|
|
169
|
+
return {
|
|
170
|
+
severity: ["low", "moderate", "high", "critical"].includes(sev) ? sev : "moderate",
|
|
171
|
+
title: typeof v["summary"] === "string" ? v["summary"] : typeof v["id"] === "string" ? v["id"] : "Unknown vulnerability",
|
|
172
|
+
url: typeof refs[0]?.["url"] === "string" ? refs[0]["url"] : void 0
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
} catch {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function scanServer(name, entry) {
|
|
180
|
+
if (entry.source !== "npm") {
|
|
181
|
+
return {
|
|
182
|
+
server: name,
|
|
183
|
+
source: entry.source,
|
|
184
|
+
score: null,
|
|
185
|
+
riskLevel: "UNKNOWN",
|
|
186
|
+
vulnerabilities: [],
|
|
187
|
+
metadata: null
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const cached = getCachedReport(name, entry.version);
|
|
191
|
+
if (cached) return cached;
|
|
192
|
+
const [metadata, vulnerabilities] = await Promise.all([
|
|
193
|
+
fetchNpmMetadata(name),
|
|
194
|
+
fetchVulnerabilities(name, entry.version)
|
|
195
|
+
]);
|
|
196
|
+
const { computeTrustScore } = await import("./trust-scorer-LYC6KZCD.js");
|
|
197
|
+
const { score, riskLevel } = computeTrustScore(metadata, vulnerabilities);
|
|
198
|
+
const report = {
|
|
199
|
+
server: name,
|
|
200
|
+
source: "npm",
|
|
201
|
+
score,
|
|
202
|
+
riskLevel,
|
|
203
|
+
vulnerabilities,
|
|
204
|
+
metadata
|
|
205
|
+
};
|
|
206
|
+
cacheReport(name, entry.version, report);
|
|
207
|
+
return report;
|
|
208
|
+
}
|
|
209
|
+
async function scanAllServers(servers, concurrency = 3) {
|
|
210
|
+
const entries = Object.entries(servers);
|
|
211
|
+
const results = [];
|
|
212
|
+
const executing = /* @__PURE__ */ new Set();
|
|
213
|
+
for (const [name, entry] of entries) {
|
|
214
|
+
const p10 = scanServer(name, entry).then((r) => {
|
|
215
|
+
results.push(r);
|
|
216
|
+
executing.delete(p10);
|
|
217
|
+
});
|
|
218
|
+
executing.add(p10);
|
|
219
|
+
if (executing.size >= concurrency) await Promise.race(executing);
|
|
220
|
+
}
|
|
221
|
+
await Promise.all(executing);
|
|
222
|
+
return results;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/core/version-checker.ts
|
|
226
|
+
function compareVersions(a, b) {
|
|
227
|
+
const aParts = a.replace(/^v/, "").split(".").map(Number);
|
|
228
|
+
const bParts = b.replace(/^v/, "").split(".").map(Number);
|
|
229
|
+
const len = Math.max(aParts.length, bParts.length);
|
|
230
|
+
for (let i = 0; i < len; i++) {
|
|
231
|
+
const aN = aParts[i] ?? 0;
|
|
232
|
+
const bN = bParts[i] ?? 0;
|
|
233
|
+
if (Number.isNaN(aN) || Number.isNaN(bN)) return 0;
|
|
234
|
+
if (aN < bN) return -1;
|
|
235
|
+
if (aN > bN) return 1;
|
|
236
|
+
}
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
function detectUpdateType(current, latest) {
|
|
240
|
+
const cParts = current.replace(/^v/, "").split(".").map(Number);
|
|
241
|
+
const lParts = latest.replace(/^v/, "").split(".").map(Number);
|
|
242
|
+
if ((lParts[0] ?? 0) > (cParts[0] ?? 0)) return "major";
|
|
243
|
+
if ((lParts[1] ?? 0) > (cParts[1] ?? 0)) return "minor";
|
|
244
|
+
return "patch";
|
|
245
|
+
}
|
|
246
|
+
async function fetchNpmLatest(packageName) {
|
|
247
|
+
try {
|
|
248
|
+
const res = await fetch(
|
|
249
|
+
`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
|
|
250
|
+
{
|
|
251
|
+
headers: { Accept: "application/json" },
|
|
252
|
+
signal: AbortSignal.timeout(8e3)
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
if (!res.ok) return null;
|
|
256
|
+
const data = await res.json();
|
|
257
|
+
return typeof data.version === "string" ? data.version : null;
|
|
258
|
+
} catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function fetchSmitheryLatest(name) {
|
|
263
|
+
try {
|
|
264
|
+
const res = await fetch(
|
|
265
|
+
`https://registry.smithery.ai/servers/${encodeURIComponent(name)}`,
|
|
266
|
+
{
|
|
267
|
+
headers: { Accept: "application/json" },
|
|
268
|
+
signal: AbortSignal.timeout(8e3)
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
if (!res.ok) return null;
|
|
272
|
+
const data = await res.json();
|
|
273
|
+
return typeof data.version === "string" ? data.version : null;
|
|
274
|
+
} catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function fetchGithubLatest(resolved) {
|
|
279
|
+
const match = resolved.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
280
|
+
if (!match) return null;
|
|
281
|
+
const [, owner, repo] = match;
|
|
282
|
+
try {
|
|
283
|
+
const res = await fetch(
|
|
284
|
+
`https://api.github.com/repos/${owner}/${repo}/releases/latest`,
|
|
285
|
+
{
|
|
286
|
+
headers: { Accept: "application/json" },
|
|
287
|
+
signal: AbortSignal.timeout(8e3)
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
if (!res.ok) return null;
|
|
291
|
+
const data = await res.json();
|
|
292
|
+
return typeof data.tag_name === "string" ? data.tag_name.replace(/^v/, "") : null;
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async function checkVersion(name, lockEntry) {
|
|
298
|
+
const current = lockEntry.version;
|
|
299
|
+
let latest = null;
|
|
300
|
+
if (lockEntry.source === "npm") {
|
|
301
|
+
latest = await fetchNpmLatest(name);
|
|
302
|
+
} else if (lockEntry.source === "smithery") {
|
|
303
|
+
latest = await fetchSmitheryLatest(name);
|
|
304
|
+
} else if (lockEntry.source === "github") {
|
|
305
|
+
latest = await fetchGithubLatest(lockEntry.resolved);
|
|
306
|
+
}
|
|
307
|
+
if (!latest || latest === current) {
|
|
308
|
+
return {
|
|
309
|
+
server: name,
|
|
310
|
+
source: lockEntry.source,
|
|
311
|
+
currentVersion: current,
|
|
312
|
+
latestVersion: latest ?? current,
|
|
313
|
+
hasUpdate: false
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const hasUpdate = compareVersions(current, latest) === -1;
|
|
317
|
+
return {
|
|
318
|
+
server: name,
|
|
319
|
+
source: lockEntry.source,
|
|
320
|
+
currentVersion: current,
|
|
321
|
+
latestVersion: latest,
|
|
322
|
+
hasUpdate,
|
|
323
|
+
updateType: hasUpdate ? detectUpdateType(current, latest) : void 0
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
async function checkAllVersions(lockfile) {
|
|
327
|
+
const entries = Object.entries(lockfile.servers);
|
|
328
|
+
if (entries.length === 0) return [];
|
|
329
|
+
const results = [];
|
|
330
|
+
const executing = /* @__PURE__ */ new Set();
|
|
331
|
+
for (const [name, entry] of entries) {
|
|
332
|
+
const p10 = checkVersion(name, entry).then((r) => {
|
|
333
|
+
results.push(r);
|
|
334
|
+
executing.delete(p10);
|
|
335
|
+
});
|
|
336
|
+
executing.add(p10);
|
|
337
|
+
if (executing.size >= 5) {
|
|
338
|
+
await Promise.race(executing);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
await Promise.all(executing);
|
|
342
|
+
return results;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/core/registry.ts
|
|
346
|
+
import { createHash } from "crypto";
|
|
347
|
+
function computeIntegrity(resolvedUrl) {
|
|
348
|
+
const hash = createHash("sha512").update(resolvedUrl).digest("base64");
|
|
349
|
+
return `sha512-${hash}`;
|
|
350
|
+
}
|
|
351
|
+
async function resolveFromSmithery(name) {
|
|
352
|
+
const url = `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`;
|
|
353
|
+
let data;
|
|
354
|
+
try {
|
|
355
|
+
const res = await fetch(url, {
|
|
356
|
+
headers: { Accept: "application/json" },
|
|
357
|
+
signal: AbortSignal.timeout(8e3)
|
|
358
|
+
});
|
|
359
|
+
if (res.status === 404) {
|
|
360
|
+
throw new Error(`Server '${name}' not found on Smithery registry`);
|
|
361
|
+
}
|
|
362
|
+
if (!res.ok) {
|
|
363
|
+
throw new Error(`Smithery API error: ${res.status}`);
|
|
364
|
+
}
|
|
365
|
+
data = await res.json();
|
|
366
|
+
} catch (err) {
|
|
367
|
+
if (err instanceof Error && err.message.includes("not found")) throw err;
|
|
368
|
+
throw new Error(
|
|
369
|
+
`Cannot reach Smithery registry: ${err instanceof Error ? err.message : String(err)}`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const version = typeof data.version === "string" ? data.version : "latest";
|
|
373
|
+
const command = typeof data.command === "string" ? data.command : "npx";
|
|
374
|
+
const args = Array.isArray(data.args) ? data.args : ["-y", `${name}@${version}`];
|
|
375
|
+
const envVars = Array.isArray(data.envVars) ? data.envVars : [];
|
|
376
|
+
const resolved = typeof data.resolved === "string" ? data.resolved : `smithery:${name}@${version}`;
|
|
377
|
+
return {
|
|
378
|
+
name,
|
|
379
|
+
version,
|
|
380
|
+
description: typeof data.description === "string" ? data.description : "",
|
|
381
|
+
runtime: "node",
|
|
382
|
+
command,
|
|
383
|
+
args,
|
|
384
|
+
envVars,
|
|
385
|
+
resolved
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
async function resolveFromNpm(packageName) {
|
|
389
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
|
|
390
|
+
let data;
|
|
391
|
+
try {
|
|
392
|
+
const res = await fetch(url, {
|
|
393
|
+
headers: { Accept: "application/json" },
|
|
394
|
+
signal: AbortSignal.timeout(8e3)
|
|
395
|
+
});
|
|
396
|
+
if (res.status === 404) {
|
|
397
|
+
throw new Error(`Package '${packageName}' not found on npm`);
|
|
398
|
+
}
|
|
399
|
+
if (!res.ok) {
|
|
400
|
+
throw new Error(`npm registry error: ${res.status}`);
|
|
401
|
+
}
|
|
402
|
+
data = await res.json();
|
|
403
|
+
} catch (err) {
|
|
404
|
+
if (err instanceof Error && err.message.includes("not found")) throw err;
|
|
405
|
+
throw new Error(
|
|
406
|
+
`Cannot reach npm registry: ${err instanceof Error ? err.message : String(err)}`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
const version = typeof data.version === "string" ? data.version : "latest";
|
|
410
|
+
const resolved = `https://registry.npmjs.org/${packageName}/-/${packageName.replace(/^@[^/]+\//, "")}-${version}.tgz`;
|
|
411
|
+
const mcpField = data.mcp && typeof data.mcp === "object" ? data.mcp : null;
|
|
412
|
+
const envVars = mcpField?.envVars ? mcpField.envVars : [];
|
|
413
|
+
return {
|
|
414
|
+
name: packageName,
|
|
415
|
+
version,
|
|
416
|
+
description: typeof data.description === "string" ? data.description : "",
|
|
417
|
+
runtime: "node",
|
|
418
|
+
command: "npx",
|
|
419
|
+
args: ["-y", `${packageName}@${version}`],
|
|
420
|
+
envVars,
|
|
421
|
+
resolved
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
async function resolveFromGitHub(githubUrl) {
|
|
425
|
+
const match = githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
426
|
+
if (!match) {
|
|
427
|
+
throw new Error(`Invalid GitHub URL: ${githubUrl}`);
|
|
428
|
+
}
|
|
429
|
+
const [, owner, repo] = match;
|
|
430
|
+
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/package.json`;
|
|
431
|
+
let pkgData = {};
|
|
432
|
+
try {
|
|
433
|
+
const res = await fetch(rawUrl, { signal: AbortSignal.timeout(8e3) });
|
|
434
|
+
if (res.ok) {
|
|
435
|
+
pkgData = await res.json();
|
|
436
|
+
}
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
const version = typeof pkgData.version === "string" ? pkgData.version : "main";
|
|
440
|
+
const name = typeof pkgData.name === "string" ? pkgData.name : `${owner}/${repo}`;
|
|
441
|
+
return {
|
|
442
|
+
name,
|
|
443
|
+
version,
|
|
444
|
+
description: typeof pkgData.description === "string" ? pkgData.description : "",
|
|
445
|
+
runtime: "node",
|
|
446
|
+
command: "npx",
|
|
447
|
+
args: ["-y", githubUrl],
|
|
448
|
+
envVars: [],
|
|
449
|
+
resolved: githubUrl
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/core/server-resolver.ts
|
|
454
|
+
function detectSource(input) {
|
|
455
|
+
if (input.startsWith("smithery:")) {
|
|
456
|
+
return { type: "smithery", input: input.slice(9) };
|
|
457
|
+
}
|
|
458
|
+
if (input.startsWith("https://github.com/") || input.startsWith("github.com/")) {
|
|
459
|
+
return { type: "github", input };
|
|
460
|
+
}
|
|
461
|
+
return { type: "npm", input };
|
|
462
|
+
}
|
|
463
|
+
function parseEnvFlags(envFlags) {
|
|
464
|
+
if (!envFlags) return {};
|
|
465
|
+
const flags = Array.isArray(envFlags) ? envFlags : [envFlags];
|
|
466
|
+
const result = {};
|
|
467
|
+
for (const flag of flags) {
|
|
468
|
+
const idx = flag.indexOf("=");
|
|
469
|
+
if (idx > 0) {
|
|
470
|
+
result[flag.slice(0, idx)] = flag.slice(idx + 1);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return result;
|
|
474
|
+
}
|
|
475
|
+
async function resolveServer(input) {
|
|
476
|
+
const source = detectSource(input);
|
|
477
|
+
switch (source.type) {
|
|
478
|
+
case "smithery":
|
|
479
|
+
return resolveFromSmithery(source.input);
|
|
480
|
+
case "github":
|
|
481
|
+
return resolveFromGitHub(source.input);
|
|
482
|
+
case "npm":
|
|
483
|
+
return resolveFromNpm(source.input);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/core/server-updater.ts
|
|
488
|
+
async function applyServerUpdate(serverName, lockEntry, clients) {
|
|
489
|
+
const fromVersion = lockEntry.version;
|
|
490
|
+
const input = lockEntry.source === "smithery" ? `smithery:${serverName}` : lockEntry.source === "github" ? lockEntry.resolved : serverName;
|
|
491
|
+
try {
|
|
492
|
+
const metadata = await resolveServer(input);
|
|
493
|
+
const integrity = computeIntegrity(metadata.resolved);
|
|
494
|
+
addEntry(serverName, {
|
|
495
|
+
...lockEntry,
|
|
496
|
+
version: metadata.version,
|
|
497
|
+
resolved: metadata.resolved,
|
|
498
|
+
integrity,
|
|
499
|
+
command: metadata.command,
|
|
500
|
+
args: metadata.args,
|
|
501
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
502
|
+
});
|
|
503
|
+
const targetClients = clients.filter(
|
|
504
|
+
(c) => lockEntry.clients.includes(c.type)
|
|
505
|
+
);
|
|
506
|
+
for (const client of targetClients) {
|
|
507
|
+
try {
|
|
508
|
+
await client.addServer(serverName, {
|
|
509
|
+
command: metadata.command,
|
|
510
|
+
args: metadata.args
|
|
511
|
+
});
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
server: serverName,
|
|
517
|
+
success: true,
|
|
518
|
+
fromVersion,
|
|
519
|
+
toVersion: metadata.version
|
|
520
|
+
};
|
|
521
|
+
} catch (err) {
|
|
522
|
+
return {
|
|
523
|
+
server: serverName,
|
|
524
|
+
success: false,
|
|
525
|
+
fromVersion,
|
|
526
|
+
toVersion: fromVersion,
|
|
527
|
+
error: err instanceof Error ? err.message : String(err)
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/commands/audit.ts
|
|
533
|
+
function colorRisk(level, score) {
|
|
534
|
+
const label = score !== null ? `${score}/100 (${level})` : level;
|
|
535
|
+
if (level === "LOW") return pc.green(label);
|
|
536
|
+
if (level === "MEDIUM") return pc.yellow(label);
|
|
537
|
+
if (level === "HIGH") return pc.red(label);
|
|
538
|
+
if (level === "CRITICAL") return pc.bold(pc.red(label));
|
|
539
|
+
return pc.dim(label);
|
|
540
|
+
}
|
|
541
|
+
function daysAgo(isoDate) {
|
|
542
|
+
const days = Math.floor((Date.now() - new Date(isoDate).getTime()) / 864e5);
|
|
543
|
+
if (days === 0) return "today";
|
|
544
|
+
if (days === 1) return "1 day ago";
|
|
545
|
+
return `${days} days ago`;
|
|
546
|
+
}
|
|
547
|
+
function countVulns(vulns) {
|
|
548
|
+
const c = { critical: 0, high: 0, moderate: 0, low: 0 };
|
|
549
|
+
for (const v of vulns) c[v.severity]++;
|
|
550
|
+
if (vulns.length === 0) return pc.green("none");
|
|
551
|
+
const parts = [];
|
|
552
|
+
if (c.critical) parts.push(pc.bold(pc.red(`${c.critical} critical`)));
|
|
553
|
+
if (c.high) parts.push(pc.red(`${c.high} high`));
|
|
554
|
+
if (c.moderate) parts.push(pc.yellow(`${c.moderate} moderate`));
|
|
555
|
+
if (c.low) parts.push(pc.dim(`${c.low} low`));
|
|
556
|
+
return parts.join(", ");
|
|
557
|
+
}
|
|
558
|
+
function printReport(report) {
|
|
559
|
+
const riskColored = colorRisk(report.riskLevel, report.score);
|
|
560
|
+
const icon = report.riskLevel === "LOW" ? pc.green("\u25CF") : report.riskLevel === "MEDIUM" ? pc.yellow("\u25CF") : report.riskLevel === "UNKNOWN" ? pc.dim("\u25CB") : pc.red("\u25CF");
|
|
561
|
+
console.log(` ${icon} ${pc.bold(report.server)} Score: ${riskColored}`);
|
|
562
|
+
if (report.source !== "npm") {
|
|
563
|
+
console.log(` ${pc.dim("Non-npm source \u2014 security data unavailable")}`);
|
|
564
|
+
console.log();
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (report.metadata) {
|
|
568
|
+
const { weeklyDownloads, packageAge, lastPublish, maintainerCount, deprecated } = report.metadata;
|
|
569
|
+
const dlStr = weeklyDownloads.toLocaleString();
|
|
570
|
+
console.log(
|
|
571
|
+
` ${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]") : "")
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
console.log(` ${pc.dim("Vulnerabilities:")} ${countVulns(report.vulnerabilities)}`);
|
|
575
|
+
if (report.vulnerabilities.length > 0) {
|
|
576
|
+
for (const v of report.vulnerabilities) {
|
|
577
|
+
const sevColor = v.severity === "critical" || v.severity === "high" ? pc.red : pc.yellow;
|
|
578
|
+
const url = v.url ? pc.dim(` ${v.url}`) : "";
|
|
579
|
+
console.log(` ${sevColor("\u25B8")} [${v.severity}] ${v.title}${url}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
console.log();
|
|
583
|
+
}
|
|
584
|
+
var audit_default = defineCommand({
|
|
585
|
+
meta: {
|
|
586
|
+
name: "audit",
|
|
587
|
+
description: "Scan installed MCP servers for security vulnerabilities and trust scores"
|
|
588
|
+
},
|
|
589
|
+
args: {
|
|
590
|
+
server: {
|
|
591
|
+
type: "positional",
|
|
592
|
+
description: "Specific server to audit (omit to audit all)",
|
|
593
|
+
required: false
|
|
594
|
+
},
|
|
595
|
+
json: {
|
|
596
|
+
type: "boolean",
|
|
597
|
+
description: "Output results as JSON",
|
|
598
|
+
default: false
|
|
599
|
+
},
|
|
600
|
+
fix: {
|
|
601
|
+
type: "boolean",
|
|
602
|
+
description: "Apply updates to fix vulnerable packages",
|
|
603
|
+
default: false
|
|
604
|
+
},
|
|
605
|
+
yes: {
|
|
606
|
+
type: "boolean",
|
|
607
|
+
description: "Skip confirmation prompt (use with --fix)",
|
|
608
|
+
default: false
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
async run({ args }) {
|
|
612
|
+
const lockfile = readLockfile();
|
|
613
|
+
const { servers } = lockfile;
|
|
614
|
+
if (Object.keys(servers).length === 0) {
|
|
615
|
+
console.log(pc.dim("\n No MCP servers installed. Run mcpman install <server> to get started.\n"));
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const targets = {};
|
|
619
|
+
if (args.server) {
|
|
620
|
+
if (!servers[args.server]) {
|
|
621
|
+
console.error(pc.red(`
|
|
622
|
+
Server "${args.server}" not found in lockfile.
|
|
623
|
+
`));
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
626
|
+
targets[args.server] = servers[args.server];
|
|
627
|
+
} else {
|
|
628
|
+
Object.assign(targets, servers);
|
|
629
|
+
}
|
|
630
|
+
const spinner5 = createSpinner(`Scanning ${Object.keys(targets).length} server(s)...`).start();
|
|
631
|
+
let reports;
|
|
632
|
+
try {
|
|
633
|
+
reports = args.server ? [await scanServer(args.server, targets[args.server])] : await scanAllServers(targets);
|
|
634
|
+
} catch (err) {
|
|
635
|
+
spinner5.error({ text: "Scan failed" });
|
|
636
|
+
console.error(pc.red(String(err)));
|
|
637
|
+
process.exit(1);
|
|
638
|
+
}
|
|
639
|
+
spinner5.success({ text: `Scanned ${reports.length} server(s)` });
|
|
640
|
+
if (args.json) {
|
|
641
|
+
console.log(JSON.stringify(reports, null, 2));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
console.log(pc.bold("\n mcpman audit\n"));
|
|
645
|
+
console.log(pc.dim(" " + "\u2500".repeat(60)));
|
|
646
|
+
for (const report of reports) {
|
|
647
|
+
printReport(report);
|
|
648
|
+
}
|
|
649
|
+
console.log(pc.dim(" " + "\u2500".repeat(60)));
|
|
650
|
+
const withIssues = reports.filter(
|
|
651
|
+
(r) => r.riskLevel !== "LOW" && r.riskLevel !== "UNKNOWN"
|
|
652
|
+
);
|
|
653
|
+
const npmReports = reports.filter((r) => r.source === "npm");
|
|
654
|
+
const parts = [];
|
|
655
|
+
parts.push(`${reports.length} server(s) scanned`);
|
|
656
|
+
if (npmReports.length < reports.length) {
|
|
657
|
+
parts.push(pc.dim(`${reports.length - npmReports.length} non-npm (unverified)`));
|
|
658
|
+
}
|
|
659
|
+
if (withIssues.length > 0) {
|
|
660
|
+
parts.push(pc.yellow(`${withIssues.length} with issues`));
|
|
661
|
+
} else {
|
|
662
|
+
parts.push(pc.green("all clear"));
|
|
663
|
+
}
|
|
664
|
+
console.log(`
|
|
665
|
+
Summary: ${parts.join(" | ")}
|
|
666
|
+
`);
|
|
667
|
+
if (args.fix) {
|
|
668
|
+
await runAuditFix(reports, lockfile.servers, args.yes);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
async function loadClients() {
|
|
673
|
+
try {
|
|
674
|
+
const mod = await import("./client-detector-SUIJSIYM.js");
|
|
675
|
+
return mod.getInstalledClients();
|
|
676
|
+
} catch {
|
|
677
|
+
return [];
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function runAuditFix(reports, servers, skipConfirm) {
|
|
681
|
+
const npmWithVulns = reports.filter(
|
|
682
|
+
(r) => r.vulnerabilities.length > 0 && r.source === "npm"
|
|
683
|
+
);
|
|
684
|
+
const nonNpmWithVulns = reports.filter(
|
|
685
|
+
(r) => r.vulnerabilities.length > 0 && r.source !== "npm"
|
|
686
|
+
);
|
|
687
|
+
if (nonNpmWithVulns.length > 0) {
|
|
688
|
+
console.log(pc.yellow(" Non-npm servers require manual update:"));
|
|
689
|
+
for (const r of nonNpmWithVulns) {
|
|
690
|
+
console.log(` ${pc.dim("\u2192")} ${r.server} (${r.source})`);
|
|
691
|
+
}
|
|
692
|
+
console.log();
|
|
693
|
+
}
|
|
694
|
+
if (npmWithVulns.length === 0) {
|
|
695
|
+
console.log(pc.green(" No fixable vulnerabilities found.\n"));
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const versionSpinner = createSpinner("Checking for available updates...").start();
|
|
699
|
+
const versionChecks = await Promise.all(
|
|
700
|
+
npmWithVulns.map((r) => checkVersion(r.server, servers[r.server]))
|
|
701
|
+
);
|
|
702
|
+
versionSpinner.success({ text: "Version check complete" });
|
|
703
|
+
const updatable = versionChecks.filter((u) => u.hasUpdate);
|
|
704
|
+
if (updatable.length === 0) {
|
|
705
|
+
console.log(pc.yellow(
|
|
706
|
+
" Vulnerable servers have no newer versions available yet.\n Allow time for registry to publish fixes.\n"
|
|
707
|
+
));
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
console.log(pc.bold(`
|
|
711
|
+
${updatable.length} server(s) can be updated to fix vulnerabilities:
|
|
712
|
+
`));
|
|
713
|
+
for (const u of updatable) {
|
|
714
|
+
console.log(` ${pc.cyan("\u2192")} ${u.server} ${pc.dim(u.currentVersion)} \u2192 ${pc.green(u.latestVersion)}`);
|
|
715
|
+
}
|
|
716
|
+
console.log();
|
|
717
|
+
if (!skipConfirm) {
|
|
718
|
+
const confirmed = await p.confirm({
|
|
719
|
+
message: `Update ${updatable.length} vulnerable server(s)?`,
|
|
720
|
+
initialValue: true
|
|
721
|
+
});
|
|
722
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
723
|
+
p.outro("Cancelled.");
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const clients = await loadClients();
|
|
728
|
+
let successCount = 0;
|
|
729
|
+
const results = [];
|
|
730
|
+
for (const u of updatable) {
|
|
731
|
+
const s = createSpinner(`Updating ${u.server}...`).start();
|
|
732
|
+
const result = await applyServerUpdate(u.server, servers[u.server], clients);
|
|
733
|
+
if (result.success) {
|
|
734
|
+
s.success({ text: `${pc.green("\u2713")} ${u.server}: ${result.fromVersion} \u2192 ${result.toVersion}` });
|
|
735
|
+
successCount++;
|
|
736
|
+
} else {
|
|
737
|
+
s.error({ text: `${pc.red("\u2717")} ${u.server}: ${result.error}` });
|
|
738
|
+
}
|
|
739
|
+
results.push({
|
|
740
|
+
server: u.server,
|
|
741
|
+
from: result.fromVersion,
|
|
742
|
+
to: result.toVersion,
|
|
743
|
+
ok: result.success,
|
|
744
|
+
error: result.error
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
console.log();
|
|
748
|
+
if (successCount > 0) {
|
|
749
|
+
const updatedNames = results.filter((r) => r.ok).map((r) => r.server);
|
|
750
|
+
const freshLockfile = readLockfile();
|
|
751
|
+
const rescanSpinner = createSpinner("Re-scanning updated servers...").start();
|
|
752
|
+
const afterReports = await Promise.all(
|
|
753
|
+
updatedNames.map((name) => scanServer(name, freshLockfile.servers[name]))
|
|
754
|
+
);
|
|
755
|
+
rescanSpinner.success({ text: "Re-scan complete" });
|
|
756
|
+
console.log(pc.bold("\n Before / After:\n"));
|
|
757
|
+
for (const after of afterReports) {
|
|
758
|
+
const before = reports.find((r) => r.server === after.server);
|
|
759
|
+
const beforeVulns = before?.vulnerabilities.length ?? 0;
|
|
760
|
+
const afterVulns = after.vulnerabilities.length;
|
|
761
|
+
const improved = afterVulns < beforeVulns ? pc.green("improved") : pc.yellow("unchanged");
|
|
762
|
+
console.log(
|
|
763
|
+
` ${pc.bold(after.server)} vulns: ${beforeVulns} \u2192 ${afterVulns} [${improved}]`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
console.log();
|
|
767
|
+
}
|
|
768
|
+
console.log(`
|
|
769
|
+
${successCount} of ${updatable.length} server(s) updated.
|
|
770
|
+
`);
|
|
771
|
+
}
|
|
8
772
|
|
|
9
773
|
// src/commands/doctor.ts
|
|
10
|
-
import { defineCommand } from "citty";
|
|
11
|
-
import
|
|
774
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
775
|
+
import pc2 from "picocolors";
|
|
12
776
|
|
|
13
777
|
// src/core/server-inventory.ts
|
|
14
778
|
async function getInstalledServers(clientFilter) {
|
|
@@ -272,12 +1036,12 @@ async function quickHealthProbe(config, timeoutMs = 3e3) {
|
|
|
272
1036
|
|
|
273
1037
|
// src/commands/doctor.ts
|
|
274
1038
|
var CHECK_ICON = {
|
|
275
|
-
pass:
|
|
276
|
-
fail:
|
|
277
|
-
skip:
|
|
278
|
-
warn:
|
|
1039
|
+
pass: pc2.green("\u2713"),
|
|
1040
|
+
fail: pc2.red("\u2717"),
|
|
1041
|
+
skip: pc2.dim("-"),
|
|
1042
|
+
warn: pc2.yellow("\u26A0")
|
|
279
1043
|
};
|
|
280
|
-
var doctor_default =
|
|
1044
|
+
var doctor_default = defineCommand2({
|
|
281
1045
|
meta: {
|
|
282
1046
|
name: "doctor",
|
|
283
1047
|
description: "Check MCP server health and configuration"
|
|
@@ -290,10 +1054,10 @@ var doctor_default = defineCommand({
|
|
|
290
1054
|
}
|
|
291
1055
|
},
|
|
292
1056
|
async run({ args }) {
|
|
293
|
-
console.log(
|
|
1057
|
+
console.log(pc2.bold("\n mcpman doctor\n"));
|
|
294
1058
|
const servers = await getInstalledServers();
|
|
295
1059
|
if (servers.length === 0) {
|
|
296
|
-
console.log(
|
|
1060
|
+
console.log(pc2.dim(" No MCP servers installed. Run mcpman install <server> to get started."));
|
|
297
1061
|
return;
|
|
298
1062
|
}
|
|
299
1063
|
const tasks = servers.map((s) => () => checkServerHealth(s.name, s.config));
|
|
@@ -305,14 +1069,14 @@ var doctor_default = defineCommand({
|
|
|
305
1069
|
if (result.status === "healthy") passed++;
|
|
306
1070
|
else failed++;
|
|
307
1071
|
}
|
|
308
|
-
console.log(
|
|
1072
|
+
console.log(pc2.dim(" " + "\u2500".repeat(50)));
|
|
309
1073
|
const parts = [];
|
|
310
|
-
if (passed > 0) parts.push(
|
|
311
|
-
if (failed > 0) parts.push(
|
|
1074
|
+
if (passed > 0) parts.push(pc2.green(`${passed} healthy`));
|
|
1075
|
+
if (failed > 0) parts.push(pc2.red(`${failed} unhealthy`));
|
|
312
1076
|
console.log(` Summary: ${parts.join(", ")}`);
|
|
313
1077
|
if (failed > 0) {
|
|
314
1078
|
if (!args.fix) {
|
|
315
|
-
console.log(
|
|
1079
|
+
console.log(pc2.dim(` Run ${pc2.cyan("mcpman doctor --fix")} for fix suggestions.
|
|
316
1080
|
`));
|
|
317
1081
|
}
|
|
318
1082
|
process.exit(1);
|
|
@@ -321,13 +1085,13 @@ var doctor_default = defineCommand({
|
|
|
321
1085
|
}
|
|
322
1086
|
});
|
|
323
1087
|
function printServerResult(result, showFix) {
|
|
324
|
-
const icon = result.status === "healthy" ?
|
|
325
|
-
console.log(` ${icon} ${
|
|
1088
|
+
const icon = result.status === "healthy" ? pc2.green("\u25CF") : pc2.red("\u25CF");
|
|
1089
|
+
console.log(` ${icon} ${pc2.bold(result.serverName)}`);
|
|
326
1090
|
for (const check of result.checks) {
|
|
327
1091
|
const checkIcon = check.skipped ? CHECK_ICON.skip : check.passed ? CHECK_ICON.pass : CHECK_ICON.fail;
|
|
328
1092
|
console.log(` ${checkIcon} ${check.name}: ${check.message}`);
|
|
329
1093
|
if (showFix && !check.passed && !check.skipped && check.fix) {
|
|
330
|
-
console.log(` ${
|
|
1094
|
+
console.log(` ${pc2.yellow("\u2192")} Fix: ${pc2.cyan(check.fix)}`);
|
|
331
1095
|
}
|
|
332
1096
|
}
|
|
333
1097
|
console.log();
|
|
@@ -336,11 +1100,11 @@ async function runParallel(tasks, concurrency) {
|
|
|
336
1100
|
const results = [];
|
|
337
1101
|
const executing = /* @__PURE__ */ new Set();
|
|
338
1102
|
for (const task of tasks) {
|
|
339
|
-
const
|
|
1103
|
+
const p10 = task().then((r) => {
|
|
340
1104
|
results.push(r);
|
|
341
|
-
executing.delete(
|
|
1105
|
+
executing.delete(p10);
|
|
342
1106
|
});
|
|
343
|
-
executing.add(
|
|
1107
|
+
executing.add(p10);
|
|
344
1108
|
if (executing.size >= concurrency) {
|
|
345
1109
|
await Promise.race(executing);
|
|
346
1110
|
}
|
|
@@ -350,182 +1114,10 @@ async function runParallel(tasks, concurrency) {
|
|
|
350
1114
|
}
|
|
351
1115
|
|
|
352
1116
|
// src/commands/init.ts
|
|
353
|
-
import { defineCommand as
|
|
354
|
-
import * as
|
|
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
|
-
}
|
|
418
|
-
|
|
419
|
-
// src/core/registry.ts
|
|
420
|
-
import { createHash } from "crypto";
|
|
421
|
-
function computeIntegrity(resolvedUrl) {
|
|
422
|
-
const hash = createHash("sha512").update(resolvedUrl).digest("base64");
|
|
423
|
-
return `sha512-${hash}`;
|
|
424
|
-
}
|
|
425
|
-
async function resolveFromSmithery(name) {
|
|
426
|
-
const url = `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`;
|
|
427
|
-
let data;
|
|
428
|
-
try {
|
|
429
|
-
const res = await fetch(url, {
|
|
430
|
-
headers: { Accept: "application/json" },
|
|
431
|
-
signal: AbortSignal.timeout(8e3)
|
|
432
|
-
});
|
|
433
|
-
if (res.status === 404) {
|
|
434
|
-
throw new Error(`Server '${name}' not found on Smithery registry`);
|
|
435
|
-
}
|
|
436
|
-
if (!res.ok) {
|
|
437
|
-
throw new Error(`Smithery API error: ${res.status}`);
|
|
438
|
-
}
|
|
439
|
-
data = await res.json();
|
|
440
|
-
} catch (err) {
|
|
441
|
-
if (err instanceof Error && err.message.includes("not found")) throw err;
|
|
442
|
-
throw new Error(
|
|
443
|
-
`Cannot reach Smithery registry: ${err instanceof Error ? err.message : String(err)}`
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
const version = typeof data.version === "string" ? data.version : "latest";
|
|
447
|
-
const command = typeof data.command === "string" ? data.command : "npx";
|
|
448
|
-
const args = Array.isArray(data.args) ? data.args : ["-y", `${name}@${version}`];
|
|
449
|
-
const envVars = Array.isArray(data.envVars) ? data.envVars : [];
|
|
450
|
-
const resolved = typeof data.resolved === "string" ? data.resolved : `smithery:${name}@${version}`;
|
|
451
|
-
return {
|
|
452
|
-
name,
|
|
453
|
-
version,
|
|
454
|
-
description: typeof data.description === "string" ? data.description : "",
|
|
455
|
-
runtime: "node",
|
|
456
|
-
command,
|
|
457
|
-
args,
|
|
458
|
-
envVars,
|
|
459
|
-
resolved
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
async function resolveFromNpm(packageName) {
|
|
463
|
-
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
|
|
464
|
-
let data;
|
|
465
|
-
try {
|
|
466
|
-
const res = await fetch(url, {
|
|
467
|
-
headers: { Accept: "application/json" },
|
|
468
|
-
signal: AbortSignal.timeout(8e3)
|
|
469
|
-
});
|
|
470
|
-
if (res.status === 404) {
|
|
471
|
-
throw new Error(`Package '${packageName}' not found on npm`);
|
|
472
|
-
}
|
|
473
|
-
if (!res.ok) {
|
|
474
|
-
throw new Error(`npm registry error: ${res.status}`);
|
|
475
|
-
}
|
|
476
|
-
data = await res.json();
|
|
477
|
-
} catch (err) {
|
|
478
|
-
if (err instanceof Error && err.message.includes("not found")) throw err;
|
|
479
|
-
throw new Error(
|
|
480
|
-
`Cannot reach npm registry: ${err instanceof Error ? err.message : String(err)}`
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
|
-
const version = typeof data.version === "string" ? data.version : "latest";
|
|
484
|
-
const resolved = `https://registry.npmjs.org/${packageName}/-/${packageName.replace(/^@[^/]+\//, "")}-${version}.tgz`;
|
|
485
|
-
const mcpField = data.mcp && typeof data.mcp === "object" ? data.mcp : null;
|
|
486
|
-
const envVars = mcpField?.envVars ? mcpField.envVars : [];
|
|
487
|
-
return {
|
|
488
|
-
name: packageName,
|
|
489
|
-
version,
|
|
490
|
-
description: typeof data.description === "string" ? data.description : "",
|
|
491
|
-
runtime: "node",
|
|
492
|
-
command: "npx",
|
|
493
|
-
args: ["-y", `${packageName}@${version}`],
|
|
494
|
-
envVars,
|
|
495
|
-
resolved
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
async function resolveFromGitHub(githubUrl) {
|
|
499
|
-
const match = githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
500
|
-
if (!match) {
|
|
501
|
-
throw new Error(`Invalid GitHub URL: ${githubUrl}`);
|
|
502
|
-
}
|
|
503
|
-
const [, owner, repo] = match;
|
|
504
|
-
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/package.json`;
|
|
505
|
-
let pkgData = {};
|
|
506
|
-
try {
|
|
507
|
-
const res = await fetch(rawUrl, { signal: AbortSignal.timeout(8e3) });
|
|
508
|
-
if (res.ok) {
|
|
509
|
-
pkgData = await res.json();
|
|
510
|
-
}
|
|
511
|
-
} catch {
|
|
512
|
-
}
|
|
513
|
-
const version = typeof pkgData.version === "string" ? pkgData.version : "main";
|
|
514
|
-
const name = typeof pkgData.name === "string" ? pkgData.name : `${owner}/${repo}`;
|
|
515
|
-
return {
|
|
516
|
-
name,
|
|
517
|
-
version,
|
|
518
|
-
description: typeof pkgData.description === "string" ? pkgData.description : "",
|
|
519
|
-
runtime: "node",
|
|
520
|
-
command: "npx",
|
|
521
|
-
args: ["-y", githubUrl],
|
|
522
|
-
envVars: [],
|
|
523
|
-
resolved: githubUrl
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// src/commands/init.ts
|
|
528
|
-
var init_default = defineCommand2({
|
|
1117
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
1118
|
+
import * as p2 from "@clack/prompts";
|
|
1119
|
+
import path3 from "path";
|
|
1120
|
+
var init_default = defineCommand3({
|
|
529
1121
|
meta: {
|
|
530
1122
|
name: "init",
|
|
531
1123
|
description: "Initialize mcpman.lock in the current project"
|
|
@@ -540,17 +1132,17 @@ var init_default = defineCommand2({
|
|
|
540
1132
|
},
|
|
541
1133
|
async run({ args }) {
|
|
542
1134
|
const nonInteractive = args.yes || !process.stdout.isTTY;
|
|
543
|
-
|
|
544
|
-
const targetPath =
|
|
1135
|
+
p2.intro("mcpman init");
|
|
1136
|
+
const targetPath = path3.join(process.cwd(), LOCKFILE_NAME);
|
|
545
1137
|
const existing = findLockfile();
|
|
546
1138
|
if (existing) {
|
|
547
1139
|
if (nonInteractive) {
|
|
548
|
-
|
|
1140
|
+
p2.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
|
|
549
1141
|
} else {
|
|
550
|
-
|
|
551
|
-
const overwrite = await
|
|
552
|
-
if (
|
|
553
|
-
|
|
1142
|
+
p2.log.warn(`Lockfile already exists: ${existing}`);
|
|
1143
|
+
const overwrite = await p2.confirm({ message: "Overwrite?" });
|
|
1144
|
+
if (p2.isCancel(overwrite) || !overwrite) {
|
|
1145
|
+
p2.outro("Cancelled.");
|
|
554
1146
|
return;
|
|
555
1147
|
}
|
|
556
1148
|
}
|
|
@@ -560,7 +1152,7 @@ var init_default = defineCommand2({
|
|
|
560
1152
|
const mod = await import("./client-detector-SUIJSIYM.js");
|
|
561
1153
|
clients = await mod.getInstalledClients();
|
|
562
1154
|
} catch {
|
|
563
|
-
|
|
1155
|
+
p2.log.warn("Could not detect AI clients \u2014 creating empty lockfile.");
|
|
564
1156
|
}
|
|
565
1157
|
const clientServers = [];
|
|
566
1158
|
for (const client of clients) {
|
|
@@ -574,26 +1166,26 @@ var init_default = defineCommand2({
|
|
|
574
1166
|
}
|
|
575
1167
|
createEmptyLockfile(targetPath);
|
|
576
1168
|
if (clientServers.length === 0) {
|
|
577
|
-
|
|
578
|
-
|
|
1169
|
+
p2.log.info("No existing servers found in any client config.");
|
|
1170
|
+
p2.outro(`Created ${LOCKFILE_NAME} \u2014 add it to version control!`);
|
|
579
1171
|
return;
|
|
580
1172
|
}
|
|
581
1173
|
let selected;
|
|
582
1174
|
if (nonInteractive) {
|
|
583
1175
|
selected = clientServers.map((cs) => cs.client.type);
|
|
584
|
-
|
|
1176
|
+
p2.log.info(`Non-interactive mode: importing all ${clientServers.length} client(s).`);
|
|
585
1177
|
} else {
|
|
586
1178
|
const options = clientServers.map((cs) => ({
|
|
587
1179
|
value: cs.client.type,
|
|
588
1180
|
label: `${cs.client.displayName} (${Object.keys(cs.servers).length} servers)`
|
|
589
1181
|
}));
|
|
590
|
-
const toImport = await
|
|
1182
|
+
const toImport = await p2.multiselect({
|
|
591
1183
|
message: "Import existing servers into lockfile?",
|
|
592
1184
|
options,
|
|
593
1185
|
required: false
|
|
594
1186
|
});
|
|
595
|
-
if (
|
|
596
|
-
|
|
1187
|
+
if (p2.isCancel(toImport)) {
|
|
1188
|
+
p2.outro(`Created empty ${LOCKFILE_NAME}`);
|
|
597
1189
|
return;
|
|
598
1190
|
}
|
|
599
1191
|
selected = toImport;
|
|
@@ -619,54 +1211,52 @@ var init_default = defineCommand2({
|
|
|
619
1211
|
importCount++;
|
|
620
1212
|
}
|
|
621
1213
|
}
|
|
622
|
-
|
|
1214
|
+
p2.outro(
|
|
623
1215
|
`Created ${LOCKFILE_NAME} with ${importCount} server(s) \u2014 commit to version control!`
|
|
624
1216
|
);
|
|
625
1217
|
}
|
|
626
1218
|
});
|
|
627
1219
|
|
|
628
1220
|
// src/commands/install.ts
|
|
629
|
-
import { defineCommand as
|
|
1221
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
630
1222
|
|
|
631
1223
|
// src/core/installer.ts
|
|
632
|
-
import * as
|
|
1224
|
+
import * as p4 from "@clack/prompts";
|
|
633
1225
|
|
|
634
|
-
// src/core/
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
return { type: "npm", input };
|
|
643
|
-
}
|
|
644
|
-
function parseEnvFlags(envFlags) {
|
|
645
|
-
if (!envFlags) return {};
|
|
646
|
-
const flags = Array.isArray(envFlags) ? envFlags : [envFlags];
|
|
647
|
-
const result = {};
|
|
648
|
-
for (const flag of flags) {
|
|
649
|
-
const idx = flag.indexOf("=");
|
|
650
|
-
if (idx > 0) {
|
|
651
|
-
result[flag.slice(0, idx)] = flag.slice(idx + 1);
|
|
1226
|
+
// src/core/installer-vault-helpers.ts
|
|
1227
|
+
import * as p3 from "@clack/prompts";
|
|
1228
|
+
async function tryLoadVaultSecrets(serverName) {
|
|
1229
|
+
try {
|
|
1230
|
+
const entries = listSecrets(serverName);
|
|
1231
|
+
if (entries.length === 0 || entries[0].keys.length === 0) {
|
|
1232
|
+
return {};
|
|
652
1233
|
}
|
|
1234
|
+
const password = await getMasterPassword();
|
|
1235
|
+
return getSecretsForServer(serverName, password);
|
|
1236
|
+
} catch {
|
|
1237
|
+
return {};
|
|
653
1238
|
}
|
|
654
|
-
return result;
|
|
655
1239
|
}
|
|
656
|
-
async function
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
1240
|
+
async function offerVaultSave(serverName, newVars, yes) {
|
|
1241
|
+
if (Object.keys(newVars).length === 0) return;
|
|
1242
|
+
if (yes) return;
|
|
1243
|
+
try {
|
|
1244
|
+
const save = await p3.confirm({
|
|
1245
|
+
message: `Save ${Object.keys(newVars).length} env var(s) to encrypted vault for future installs?`
|
|
1246
|
+
});
|
|
1247
|
+
if (p3.isCancel(save) || !save) return;
|
|
1248
|
+
const password = await getMasterPassword();
|
|
1249
|
+
for (const [key, value] of Object.entries(newVars)) {
|
|
1250
|
+
setSecret(serverName, key, value, password);
|
|
1251
|
+
}
|
|
1252
|
+
p3.log.success(`Credentials saved to vault for '${serverName}'`);
|
|
1253
|
+
} catch (err) {
|
|
1254
|
+
p3.log.warn(`Could not save to vault: ${err instanceof Error ? err.message : String(err)}`);
|
|
665
1255
|
}
|
|
666
1256
|
}
|
|
667
1257
|
|
|
668
1258
|
// src/core/installer.ts
|
|
669
|
-
async function
|
|
1259
|
+
async function loadClients2() {
|
|
670
1260
|
try {
|
|
671
1261
|
const mod = await import("./client-detector-SUIJSIYM.js");
|
|
672
1262
|
return mod.getInstalledClients();
|
|
@@ -675,83 +1265,86 @@ async function loadClients() {
|
|
|
675
1265
|
}
|
|
676
1266
|
}
|
|
677
1267
|
async function installServer(input, options = {}) {
|
|
678
|
-
|
|
679
|
-
const
|
|
680
|
-
|
|
1268
|
+
p4.intro("mcpman install");
|
|
1269
|
+
const spinner5 = p4.spinner();
|
|
1270
|
+
spinner5.start("Resolving server...");
|
|
681
1271
|
let metadata;
|
|
682
1272
|
try {
|
|
683
1273
|
metadata = await resolveServer(input);
|
|
684
1274
|
} catch (err) {
|
|
685
|
-
|
|
686
|
-
|
|
1275
|
+
spinner5.stop("Resolution failed");
|
|
1276
|
+
p4.log.error(err instanceof Error ? err.message : String(err));
|
|
687
1277
|
process.exit(1);
|
|
688
1278
|
}
|
|
689
|
-
|
|
690
|
-
const clients = await
|
|
1279
|
+
spinner5.stop(`Found: ${metadata.name}@${metadata.version}`);
|
|
1280
|
+
const clients = await loadClients2();
|
|
691
1281
|
if (clients.length === 0) {
|
|
692
|
-
|
|
693
|
-
|
|
1282
|
+
p4.log.warn("No supported AI clients detected on this machine.");
|
|
1283
|
+
p4.log.info("Supported: Claude Desktop, Cursor, VS Code, Windsurf");
|
|
694
1284
|
process.exit(1);
|
|
695
1285
|
}
|
|
696
1286
|
let selectedClients;
|
|
697
1287
|
if (options.client) {
|
|
698
1288
|
const found = clients.find((c) => c.type === options.client || c.displayName.toLowerCase() === options.client?.toLowerCase());
|
|
699
1289
|
if (!found) {
|
|
700
|
-
|
|
701
|
-
|
|
1290
|
+
p4.log.error(`Client '${options.client}' not found or not installed.`);
|
|
1291
|
+
p4.log.info(`Available: ${clients.map((c) => c.type).join(", ")}`);
|
|
702
1292
|
process.exit(1);
|
|
703
1293
|
}
|
|
704
1294
|
selectedClients = [found];
|
|
705
1295
|
} else if (options.yes || clients.length === 1) {
|
|
706
1296
|
selectedClients = clients;
|
|
707
1297
|
} else {
|
|
708
|
-
const chosen = await
|
|
1298
|
+
const chosen = await p4.multiselect({
|
|
709
1299
|
message: "Install to which client(s)?",
|
|
710
1300
|
options: clients.map((c) => ({ value: c.type, label: c.displayName })),
|
|
711
1301
|
required: true
|
|
712
1302
|
});
|
|
713
|
-
if (
|
|
714
|
-
|
|
1303
|
+
if (p4.isCancel(chosen)) {
|
|
1304
|
+
p4.outro("Cancelled.");
|
|
715
1305
|
process.exit(0);
|
|
716
1306
|
}
|
|
717
1307
|
selectedClients = clients.filter((c) => chosen.includes(c.type));
|
|
718
1308
|
}
|
|
719
1309
|
const providedEnv = parseEnvFlags(options.env);
|
|
720
|
-
const
|
|
1310
|
+
const vaultEnv = await tryLoadVaultSecrets(metadata.name);
|
|
1311
|
+
const collectedEnv = { ...vaultEnv, ...providedEnv };
|
|
1312
|
+
const newlyEnteredVars = {};
|
|
721
1313
|
const requiredVars = metadata.envVars.filter((e) => e.required && !(e.name in collectedEnv));
|
|
722
1314
|
for (const envVar of requiredVars) {
|
|
723
1315
|
if (options.yes && envVar.default) {
|
|
724
1316
|
collectedEnv[envVar.name] = envVar.default;
|
|
725
1317
|
continue;
|
|
726
1318
|
}
|
|
727
|
-
const val = await
|
|
1319
|
+
const val = await p4.text({
|
|
728
1320
|
message: `${envVar.name}${envVar.description ? ` \u2014 ${envVar.description}` : ""}`,
|
|
729
1321
|
placeholder: envVar.default ?? "",
|
|
730
1322
|
validate: (v) => envVar.required && !v ? "Required" : void 0
|
|
731
1323
|
});
|
|
732
|
-
if (
|
|
733
|
-
|
|
1324
|
+
if (p4.isCancel(val)) {
|
|
1325
|
+
p4.outro("Cancelled.");
|
|
734
1326
|
process.exit(0);
|
|
735
1327
|
}
|
|
736
1328
|
collectedEnv[envVar.name] = val;
|
|
1329
|
+
newlyEnteredVars[envVar.name] = val;
|
|
737
1330
|
}
|
|
738
1331
|
const entry = {
|
|
739
1332
|
command: metadata.command,
|
|
740
1333
|
args: metadata.args,
|
|
741
1334
|
...Object.keys(collectedEnv).length > 0 ? { env: collectedEnv } : {}
|
|
742
1335
|
};
|
|
743
|
-
|
|
1336
|
+
spinner5.start("Writing config...");
|
|
744
1337
|
const clientTypes = [];
|
|
745
1338
|
for (const client of selectedClients) {
|
|
746
1339
|
try {
|
|
747
1340
|
await client.addServer(metadata.name, entry);
|
|
748
1341
|
clientTypes.push(client.type);
|
|
749
1342
|
} catch (err) {
|
|
750
|
-
|
|
751
|
-
|
|
1343
|
+
spinner5.stop("Partial failure");
|
|
1344
|
+
p4.log.warn(`Failed to write to ${client.displayName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
752
1345
|
}
|
|
753
1346
|
}
|
|
754
|
-
|
|
1347
|
+
spinner5.stop("Config written");
|
|
755
1348
|
const source = detectSource(input);
|
|
756
1349
|
const integrity = computeIntegrity(metadata.resolved);
|
|
757
1350
|
addEntry(metadata.name, {
|
|
@@ -767,12 +1360,13 @@ async function installServer(input, options = {}) {
|
|
|
767
1360
|
clients: clientTypes
|
|
768
1361
|
});
|
|
769
1362
|
const lockPath = findLockfile() ?? "mcpman.lock (global)";
|
|
770
|
-
|
|
771
|
-
|
|
1363
|
+
p4.log.success(`Lockfile updated: ${lockPath}`);
|
|
1364
|
+
await offerVaultSave(metadata.name, newlyEnteredVars, options.yes ?? false);
|
|
1365
|
+
p4.outro(`${metadata.name}@${metadata.version} installed to ${clientTypes.join(", ")}`);
|
|
772
1366
|
}
|
|
773
1367
|
|
|
774
1368
|
// src/utils/logger.ts
|
|
775
|
-
import
|
|
1369
|
+
import pc3 from "picocolors";
|
|
776
1370
|
var noColor = process.env.NO_COLOR !== void 0 || process.argv.includes("--no-color");
|
|
777
1371
|
var isVerbose = process.argv.includes("--verbose");
|
|
778
1372
|
var isJson = process.argv.includes("--json");
|
|
@@ -781,19 +1375,19 @@ function colorize(fn, text2) {
|
|
|
781
1375
|
}
|
|
782
1376
|
function info(message) {
|
|
783
1377
|
if (isJson) return;
|
|
784
|
-
console.log(`${colorize(
|
|
1378
|
+
console.log(`${colorize(pc3.cyan, "i")} ${message}`);
|
|
785
1379
|
}
|
|
786
1380
|
function error(message) {
|
|
787
1381
|
if (isJson) return;
|
|
788
|
-
console.error(`${colorize(
|
|
1382
|
+
console.error(`${colorize(pc3.red, "\u2717")} ${message}`);
|
|
789
1383
|
}
|
|
790
1384
|
function json(data) {
|
|
791
1385
|
console.log(JSON.stringify(data, null, 2));
|
|
792
1386
|
}
|
|
793
1387
|
|
|
794
1388
|
// src/commands/install.ts
|
|
795
|
-
import * as
|
|
796
|
-
var install_default =
|
|
1389
|
+
import * as p5 from "@clack/prompts";
|
|
1390
|
+
var install_default = defineCommand4({
|
|
797
1391
|
meta: {
|
|
798
1392
|
name: "install",
|
|
799
1393
|
description: "Install an MCP server into one or more AI clients"
|
|
@@ -842,8 +1436,8 @@ async function restoreFromLockfile() {
|
|
|
842
1436
|
info("Lockfile is empty \u2014 nothing to restore.");
|
|
843
1437
|
return;
|
|
844
1438
|
}
|
|
845
|
-
|
|
846
|
-
|
|
1439
|
+
p5.intro(`mcpman install (restore from ${lockPath})`);
|
|
1440
|
+
p5.log.info(`Restoring ${entries.length} server(s)...`);
|
|
847
1441
|
for (const [name, entry] of entries) {
|
|
848
1442
|
const input = entry.source === "smithery" ? `smithery:${name}` : entry.source === "github" ? entry.resolved : name;
|
|
849
1443
|
await installServer(input, {
|
|
@@ -851,18 +1445,18 @@ async function restoreFromLockfile() {
|
|
|
851
1445
|
yes: true
|
|
852
1446
|
});
|
|
853
1447
|
}
|
|
854
|
-
|
|
1448
|
+
p5.outro("Restore complete.");
|
|
855
1449
|
}
|
|
856
1450
|
|
|
857
1451
|
// src/commands/list.ts
|
|
858
|
-
import { defineCommand as
|
|
859
|
-
import
|
|
1452
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
1453
|
+
import pc4 from "picocolors";
|
|
860
1454
|
var STATUS_ICON = {
|
|
861
|
-
healthy:
|
|
862
|
-
unhealthy:
|
|
863
|
-
unknown:
|
|
1455
|
+
healthy: pc4.green("\u25CF"),
|
|
1456
|
+
unhealthy: pc4.red("\u25CF"),
|
|
1457
|
+
unknown: pc4.dim("\u25CB")
|
|
864
1458
|
};
|
|
865
|
-
var list_default =
|
|
1459
|
+
var list_default = defineCommand5({
|
|
866
1460
|
meta: {
|
|
867
1461
|
name: "list",
|
|
868
1462
|
description: "List installed MCP servers"
|
|
@@ -882,7 +1476,7 @@ var list_default = defineCommand4({
|
|
|
882
1476
|
const servers = await getInstalledServers(args.client);
|
|
883
1477
|
if (servers.length === 0) {
|
|
884
1478
|
const filter = args.client ? ` for client "${args.client}"` : "";
|
|
885
|
-
console.log(
|
|
1479
|
+
console.log(pc4.dim(`No MCP servers installed${filter}. Run ${pc4.cyan("mcpman install <server>")} to get started.`));
|
|
886
1480
|
return;
|
|
887
1481
|
}
|
|
888
1482
|
const withStatus = await Promise.all(
|
|
@@ -906,8 +1500,8 @@ var list_default = defineCommand4({
|
|
|
906
1500
|
const nameWidth = Math.max(4, ...withStatus.map((s) => s.name.length));
|
|
907
1501
|
const clientsWidth = Math.max(7, ...withStatus.map((s) => formatClients(s.clients).length));
|
|
908
1502
|
const header = ` ${pad("NAME", nameWidth)} ${pad("CLIENT(S)", clientsWidth)} ${pad("COMMAND", 20)} STATUS`;
|
|
909
|
-
console.log(
|
|
910
|
-
console.log(
|
|
1503
|
+
console.log(pc4.dim(header));
|
|
1504
|
+
console.log(pc4.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientsWidth)} ${"-".repeat(20)} ------`));
|
|
911
1505
|
for (const s of withStatus) {
|
|
912
1506
|
const icon = STATUS_ICON[s.status];
|
|
913
1507
|
const clientsStr = formatClients(s.clients);
|
|
@@ -915,7 +1509,7 @@ var list_default = defineCommand4({
|
|
|
915
1509
|
console.log(` ${pad(s.name, nameWidth)} ${pad(clientsStr, clientsWidth)} ${pad(cmdStr, 20)} ${icon} ${s.status}`);
|
|
916
1510
|
}
|
|
917
1511
|
const clientSet = new Set(withStatus.flatMap((s) => s.clients));
|
|
918
|
-
console.log(
|
|
1512
|
+
console.log(pc4.dim(`
|
|
919
1513
|
${withStatus.length} server${withStatus.length !== 1 ? "s" : ""} \xB7 ${clientSet.size} client${clientSet.size !== 1 ? "s" : ""}`));
|
|
920
1514
|
}
|
|
921
1515
|
});
|
|
@@ -936,9 +1530,9 @@ function formatClients(clients) {
|
|
|
936
1530
|
}
|
|
937
1531
|
|
|
938
1532
|
// src/commands/remove.ts
|
|
939
|
-
import { defineCommand as
|
|
940
|
-
import * as
|
|
941
|
-
import
|
|
1533
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
1534
|
+
import * as p6 from "@clack/prompts";
|
|
1535
|
+
import pc5 from "picocolors";
|
|
942
1536
|
var CLIENT_DISPLAY2 = {
|
|
943
1537
|
"claude-desktop": "Claude",
|
|
944
1538
|
cursor: "Cursor",
|
|
@@ -948,7 +1542,7 @@ var CLIENT_DISPLAY2 = {
|
|
|
948
1542
|
function clientDisplayName(type) {
|
|
949
1543
|
return CLIENT_DISPLAY2[type] ?? type;
|
|
950
1544
|
}
|
|
951
|
-
var remove_default =
|
|
1545
|
+
var remove_default = defineCommand6({
|
|
952
1546
|
meta: {
|
|
953
1547
|
name: "remove",
|
|
954
1548
|
description: "Remove an MCP server from one or more AI clients"
|
|
@@ -975,17 +1569,17 @@ var remove_default = defineCommand5({
|
|
|
975
1569
|
}
|
|
976
1570
|
},
|
|
977
1571
|
async run({ args }) {
|
|
978
|
-
|
|
1572
|
+
p6.intro(pc5.bold("mcpman remove"));
|
|
979
1573
|
const serverName = args.server;
|
|
980
1574
|
const servers = await getInstalledServers();
|
|
981
1575
|
const match = servers.find((s) => s.name === serverName);
|
|
982
1576
|
if (!match) {
|
|
983
|
-
|
|
1577
|
+
p6.log.warn(`Server "${serverName}" is not installed.`);
|
|
984
1578
|
const similar = servers.filter((s) => s.name.includes(serverName) || serverName.includes(s.name));
|
|
985
1579
|
if (similar.length > 0) {
|
|
986
|
-
|
|
1580
|
+
p6.log.info(`Did you mean: ${similar.map((s) => pc5.cyan(s.name)).join(", ")}?`);
|
|
987
1581
|
}
|
|
988
|
-
|
|
1582
|
+
p6.outro("Nothing to remove.");
|
|
989
1583
|
return;
|
|
990
1584
|
}
|
|
991
1585
|
let targetClients;
|
|
@@ -993,15 +1587,15 @@ var remove_default = defineCommand5({
|
|
|
993
1587
|
targetClients = match.clients;
|
|
994
1588
|
} else if (args.client) {
|
|
995
1589
|
if (!match.clients.includes(args.client)) {
|
|
996
|
-
|
|
997
|
-
|
|
1590
|
+
p6.log.warn(`Server "${serverName}" is not installed in client "${args.client}".`);
|
|
1591
|
+
p6.outro("Nothing to remove.");
|
|
998
1592
|
return;
|
|
999
1593
|
}
|
|
1000
1594
|
targetClients = [args.client];
|
|
1001
1595
|
} else if (match.clients.length === 1) {
|
|
1002
1596
|
targetClients = match.clients;
|
|
1003
1597
|
} else {
|
|
1004
|
-
const selected = await
|
|
1598
|
+
const selected = await p6.multiselect({
|
|
1005
1599
|
message: `Remove "${serverName}" from which clients?`,
|
|
1006
1600
|
options: match.clients.map((c) => ({
|
|
1007
1601
|
value: c,
|
|
@@ -1009,19 +1603,19 @@ var remove_default = defineCommand5({
|
|
|
1009
1603
|
})),
|
|
1010
1604
|
required: true
|
|
1011
1605
|
});
|
|
1012
|
-
if (
|
|
1013
|
-
|
|
1606
|
+
if (p6.isCancel(selected)) {
|
|
1607
|
+
p6.outro("Cancelled.");
|
|
1014
1608
|
process.exit(0);
|
|
1015
1609
|
}
|
|
1016
1610
|
targetClients = selected;
|
|
1017
1611
|
}
|
|
1018
1612
|
if (!args.yes) {
|
|
1019
1613
|
const clientNames = targetClients.map(clientDisplayName).join(", ");
|
|
1020
|
-
const confirmed = await
|
|
1021
|
-
message: `Remove ${
|
|
1614
|
+
const confirmed = await p6.confirm({
|
|
1615
|
+
message: `Remove ${pc5.cyan(serverName)} from ${pc5.yellow(clientNames)}?`
|
|
1022
1616
|
});
|
|
1023
|
-
if (
|
|
1024
|
-
|
|
1617
|
+
if (p6.isCancel(confirmed) || !confirmed) {
|
|
1618
|
+
p6.outro("Cancelled.");
|
|
1025
1619
|
return;
|
|
1026
1620
|
}
|
|
1027
1621
|
}
|
|
@@ -1035,24 +1629,597 @@ var remove_default = defineCommand5({
|
|
|
1035
1629
|
}
|
|
1036
1630
|
try {
|
|
1037
1631
|
await handler.removeServer(serverName);
|
|
1038
|
-
|
|
1632
|
+
p6.log.success(`Removed from ${clientDisplayName(clientType)}`);
|
|
1039
1633
|
} catch (err) {
|
|
1040
1634
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1041
1635
|
errors.push(`${clientDisplayName(clientType)}: ${msg}`);
|
|
1042
1636
|
}
|
|
1043
1637
|
}
|
|
1044
1638
|
if (errors.length > 0) {
|
|
1045
|
-
for (const e of errors)
|
|
1046
|
-
|
|
1639
|
+
for (const e of errors) p6.log.error(e);
|
|
1640
|
+
p6.outro(pc5.red("Completed with errors."));
|
|
1641
|
+
process.exit(1);
|
|
1642
|
+
}
|
|
1643
|
+
p6.outro(pc5.green(`Removed "${serverName}" successfully.`));
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
// src/commands/secrets.ts
|
|
1648
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
1649
|
+
import pc6 from "picocolors";
|
|
1650
|
+
import * as p7 from "@clack/prompts";
|
|
1651
|
+
function maskValue(value) {
|
|
1652
|
+
if (value.length <= 8) return "***";
|
|
1653
|
+
return `${value.slice(0, 4)}***${value.slice(-3)}`;
|
|
1654
|
+
}
|
|
1655
|
+
function parseKeyValue(input) {
|
|
1656
|
+
const idx = input.indexOf("=");
|
|
1657
|
+
if (idx <= 0) return null;
|
|
1658
|
+
return { key: input.slice(0, idx), value: input.slice(idx + 1) };
|
|
1659
|
+
}
|
|
1660
|
+
var setCommand = defineCommand7({
|
|
1661
|
+
meta: { name: "set", description: "Store an encrypted secret for a server" },
|
|
1662
|
+
args: {
|
|
1663
|
+
server: {
|
|
1664
|
+
type: "positional",
|
|
1665
|
+
description: "Server name (e.g. @modelcontextprotocol/server-github)",
|
|
1666
|
+
required: true
|
|
1667
|
+
},
|
|
1668
|
+
keyvalue: {
|
|
1669
|
+
type: "positional",
|
|
1670
|
+
description: "KEY=VALUE pair to store",
|
|
1671
|
+
required: true
|
|
1672
|
+
}
|
|
1673
|
+
},
|
|
1674
|
+
async run({ args }) {
|
|
1675
|
+
const parsed = parseKeyValue(args.keyvalue);
|
|
1676
|
+
if (!parsed) {
|
|
1677
|
+
console.error(pc6.red("\u2717") + " Invalid format. Expected KEY=VALUE");
|
|
1678
|
+
process.exit(1);
|
|
1679
|
+
}
|
|
1680
|
+
p7.intro(pc6.cyan("mcpman secrets set"));
|
|
1681
|
+
const isNew = listSecrets(args.server).length === 0 || !listSecrets(args.server)[0]?.keys.includes(parsed.key);
|
|
1682
|
+
const vaultPath = (await import("./vault-service-UTZAV6N6.js")).getVaultPath();
|
|
1683
|
+
const vaultExists = (await import("fs")).existsSync(vaultPath);
|
|
1684
|
+
const password = await getMasterPassword(!vaultExists && isNew);
|
|
1685
|
+
const spin = p7.spinner();
|
|
1686
|
+
spin.start("Encrypting secret...");
|
|
1687
|
+
try {
|
|
1688
|
+
setSecret(args.server, parsed.key, parsed.value, password);
|
|
1689
|
+
spin.stop(
|
|
1690
|
+
`${pc6.green("\u2713")} Stored ${pc6.bold(parsed.key)} for ${pc6.cyan(args.server)}`
|
|
1691
|
+
);
|
|
1692
|
+
} catch (err) {
|
|
1693
|
+
spin.stop(pc6.red("\u2717") + " Failed to store secret");
|
|
1694
|
+
console.error(pc6.dim(String(err)));
|
|
1695
|
+
process.exit(1);
|
|
1696
|
+
}
|
|
1697
|
+
p7.outro(pc6.dim("Secret encrypted and saved to vault."));
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
var listCommand = defineCommand7({
|
|
1701
|
+
meta: { name: "list", description: "List secret keys stored in the vault" },
|
|
1702
|
+
args: {
|
|
1703
|
+
server: {
|
|
1704
|
+
type: "positional",
|
|
1705
|
+
description: "Filter by server name (optional)",
|
|
1706
|
+
required: false
|
|
1707
|
+
}
|
|
1708
|
+
},
|
|
1709
|
+
async run({ args }) {
|
|
1710
|
+
const results = listSecrets(args.server || void 0);
|
|
1711
|
+
if (results.length === 0) {
|
|
1712
|
+
const filter = args.server ? ` for ${pc6.cyan(args.server)}` : "";
|
|
1713
|
+
console.log(pc6.dim(`No secrets stored${filter}.`));
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
console.log("");
|
|
1717
|
+
for (const { server, keys } of results) {
|
|
1718
|
+
console.log(pc6.bold(pc6.cyan(server)));
|
|
1719
|
+
for (const key of keys) {
|
|
1720
|
+
console.log(` ${pc6.green("\u25CF")} ${pc6.bold(key)} ${pc6.dim(maskValue("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"))}`);
|
|
1721
|
+
}
|
|
1722
|
+
console.log("");
|
|
1723
|
+
}
|
|
1724
|
+
const total = results.reduce((n, r) => n + r.keys.length, 0);
|
|
1725
|
+
console.log(pc6.dim(` ${total} secret${total !== 1 ? "s" : ""} in ${results.length} server${results.length !== 1 ? "s" : ""}`));
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
var removeCommand = defineCommand7({
|
|
1729
|
+
meta: { name: "remove", description: "Delete a secret from the vault" },
|
|
1730
|
+
args: {
|
|
1731
|
+
server: {
|
|
1732
|
+
type: "positional",
|
|
1733
|
+
description: "Server name",
|
|
1734
|
+
required: true
|
|
1735
|
+
},
|
|
1736
|
+
key: {
|
|
1737
|
+
type: "positional",
|
|
1738
|
+
description: "Secret key to remove",
|
|
1739
|
+
required: true
|
|
1740
|
+
}
|
|
1741
|
+
},
|
|
1742
|
+
async run({ args }) {
|
|
1743
|
+
const confirmed = await p7.confirm({
|
|
1744
|
+
message: `Remove ${pc6.bold(args.key)} from ${pc6.cyan(args.server)}?`,
|
|
1745
|
+
initialValue: false
|
|
1746
|
+
});
|
|
1747
|
+
if (p7.isCancel(confirmed) || !confirmed) {
|
|
1748
|
+
p7.cancel("Cancelled.");
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
try {
|
|
1752
|
+
removeSecret(args.server, args.key);
|
|
1753
|
+
console.log(`${pc6.green("\u2713")} Removed ${pc6.bold(args.key)} from ${pc6.cyan(args.server)}`);
|
|
1754
|
+
} catch (err) {
|
|
1755
|
+
console.error(pc6.red("\u2717") + " Failed to remove secret");
|
|
1756
|
+
console.error(pc6.dim(String(err)));
|
|
1757
|
+
process.exit(1);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
var secrets_default = defineCommand7({
|
|
1762
|
+
meta: {
|
|
1763
|
+
name: "secrets",
|
|
1764
|
+
description: "Manage encrypted secrets for MCP servers"
|
|
1765
|
+
},
|
|
1766
|
+
subCommands: {
|
|
1767
|
+
set: setCommand,
|
|
1768
|
+
list: listCommand,
|
|
1769
|
+
remove: removeCommand
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
// src/commands/sync.ts
|
|
1774
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
1775
|
+
import * as p8 from "@clack/prompts";
|
|
1776
|
+
import pc7 from "picocolors";
|
|
1777
|
+
|
|
1778
|
+
// src/core/config-diff.ts
|
|
1779
|
+
function reconstructServerEntry(lockEntry) {
|
|
1780
|
+
const entry = {
|
|
1781
|
+
command: lockEntry.command
|
|
1782
|
+
};
|
|
1783
|
+
if (lockEntry.args && lockEntry.args.length > 0) {
|
|
1784
|
+
entry.args = lockEntry.args;
|
|
1785
|
+
}
|
|
1786
|
+
if (lockEntry.envVars && lockEntry.envVars.length > 0) {
|
|
1787
|
+
entry.env = Object.fromEntries(lockEntry.envVars.map((k) => [k, ""]));
|
|
1788
|
+
}
|
|
1789
|
+
return entry;
|
|
1790
|
+
}
|
|
1791
|
+
function computeDiff(lockfile, clientConfigs, options = {}) {
|
|
1792
|
+
const actions = [];
|
|
1793
|
+
for (const [server, lockEntry] of Object.entries(lockfile.servers)) {
|
|
1794
|
+
for (const client of lockEntry.clients) {
|
|
1795
|
+
const config = clientConfigs.get(client);
|
|
1796
|
+
if (!config) continue;
|
|
1797
|
+
if (server in config.servers) {
|
|
1798
|
+
actions.push({ server, client, action: "ok" });
|
|
1799
|
+
} else {
|
|
1800
|
+
actions.push({
|
|
1801
|
+
server,
|
|
1802
|
+
client,
|
|
1803
|
+
action: "add",
|
|
1804
|
+
entry: reconstructServerEntry(lockEntry)
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
const extraAction = options.remove ? "remove" : "extra";
|
|
1810
|
+
for (const [client, config] of clientConfigs) {
|
|
1811
|
+
for (const server of Object.keys(config.servers)) {
|
|
1812
|
+
if (!(server in lockfile.servers)) {
|
|
1813
|
+
actions.push({ server, client, action: extraAction });
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
return actions;
|
|
1818
|
+
}
|
|
1819
|
+
function computeDiffFromClient(sourceClient, clientConfigs, options = {}) {
|
|
1820
|
+
const actions = [];
|
|
1821
|
+
const sourceConfig = clientConfigs.get(sourceClient);
|
|
1822
|
+
if (!sourceConfig) return [];
|
|
1823
|
+
const extraAction = options.remove ? "remove" : "extra";
|
|
1824
|
+
for (const [client, config] of clientConfigs) {
|
|
1825
|
+
if (client === sourceClient) continue;
|
|
1826
|
+
for (const [server, entry] of Object.entries(sourceConfig.servers)) {
|
|
1827
|
+
if (server in config.servers) {
|
|
1828
|
+
actions.push({ server, client, action: "ok" });
|
|
1829
|
+
} else {
|
|
1830
|
+
actions.push({ server, client, action: "add", entry });
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
for (const server of Object.keys(config.servers)) {
|
|
1834
|
+
if (!(server in sourceConfig.servers)) {
|
|
1835
|
+
actions.push({ server, client, action: extraAction });
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
return actions;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// src/core/sync-engine.ts
|
|
1843
|
+
async function applySyncActions(actions, clients) {
|
|
1844
|
+
const result = { applied: 0, removed: 0, failed: 0, errors: [] };
|
|
1845
|
+
const addActions = actions.filter((a) => a.action === "add" && a.entry);
|
|
1846
|
+
for (const action of addActions) {
|
|
1847
|
+
const handler = clients.get(action.client);
|
|
1848
|
+
if (!handler || !action.entry) {
|
|
1849
|
+
result.failed++;
|
|
1850
|
+
result.errors.push({
|
|
1851
|
+
server: action.server,
|
|
1852
|
+
client: action.client,
|
|
1853
|
+
error: "No handler available for client"
|
|
1854
|
+
});
|
|
1855
|
+
continue;
|
|
1856
|
+
}
|
|
1857
|
+
try {
|
|
1858
|
+
await handler.addServer(action.server, action.entry);
|
|
1859
|
+
result.applied++;
|
|
1860
|
+
} catch (err) {
|
|
1861
|
+
result.failed++;
|
|
1862
|
+
result.errors.push({
|
|
1863
|
+
server: action.server,
|
|
1864
|
+
client: action.client,
|
|
1865
|
+
error: String(err)
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
const removeActions = actions.filter((a) => a.action === "remove");
|
|
1870
|
+
for (const action of removeActions) {
|
|
1871
|
+
const handler = clients.get(action.client);
|
|
1872
|
+
if (!handler) {
|
|
1873
|
+
result.failed++;
|
|
1874
|
+
result.errors.push({
|
|
1875
|
+
server: action.server,
|
|
1876
|
+
client: action.client,
|
|
1877
|
+
error: "No handler available for client"
|
|
1878
|
+
});
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
try {
|
|
1882
|
+
await handler.removeServer(action.server);
|
|
1883
|
+
result.removed++;
|
|
1884
|
+
} catch (err) {
|
|
1885
|
+
result.failed++;
|
|
1886
|
+
result.errors.push({
|
|
1887
|
+
server: action.server,
|
|
1888
|
+
client: action.client,
|
|
1889
|
+
error: String(err)
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
return result;
|
|
1894
|
+
}
|
|
1895
|
+
async function getClientConfigs() {
|
|
1896
|
+
const configs = /* @__PURE__ */ new Map();
|
|
1897
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
1898
|
+
let installedClients;
|
|
1899
|
+
try {
|
|
1900
|
+
installedClients = await getInstalledClients();
|
|
1901
|
+
} catch {
|
|
1902
|
+
return { configs, handlers };
|
|
1903
|
+
}
|
|
1904
|
+
await Promise.all(
|
|
1905
|
+
installedClients.map(async (handler) => {
|
|
1906
|
+
try {
|
|
1907
|
+
const config = await handler.readConfig();
|
|
1908
|
+
configs.set(handler.type, config);
|
|
1909
|
+
handlers.set(handler.type, handler);
|
|
1910
|
+
} catch (err) {
|
|
1911
|
+
console.warn(`[mcpman] Warning: could not read config for ${handler.displayName}: ${String(err)}`);
|
|
1912
|
+
}
|
|
1913
|
+
})
|
|
1914
|
+
);
|
|
1915
|
+
return { configs, handlers };
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// src/commands/sync.ts
|
|
1919
|
+
var VALID_CLIENTS = ["claude-desktop", "cursor", "vscode", "windsurf"];
|
|
1920
|
+
var CLIENT_DISPLAY3 = {
|
|
1921
|
+
"claude-desktop": "Claude Desktop",
|
|
1922
|
+
cursor: "Cursor",
|
|
1923
|
+
vscode: "VS Code",
|
|
1924
|
+
windsurf: "Windsurf"
|
|
1925
|
+
};
|
|
1926
|
+
var sync_default = defineCommand8({
|
|
1927
|
+
meta: {
|
|
1928
|
+
name: "sync",
|
|
1929
|
+
description: "Sync MCP server configs across all detected AI clients"
|
|
1930
|
+
},
|
|
1931
|
+
args: {
|
|
1932
|
+
"dry-run": {
|
|
1933
|
+
type: "boolean",
|
|
1934
|
+
description: "Preview changes without applying them",
|
|
1935
|
+
default: false
|
|
1936
|
+
},
|
|
1937
|
+
remove: {
|
|
1938
|
+
type: "boolean",
|
|
1939
|
+
description: "Remove extra servers not in lockfile",
|
|
1940
|
+
default: false
|
|
1941
|
+
},
|
|
1942
|
+
source: {
|
|
1943
|
+
type: "string",
|
|
1944
|
+
description: "Use a specific client as source of truth (claude-desktop, cursor, vscode, windsurf)"
|
|
1945
|
+
},
|
|
1946
|
+
yes: {
|
|
1947
|
+
type: "boolean",
|
|
1948
|
+
description: "Skip confirmation prompt",
|
|
1949
|
+
default: false
|
|
1950
|
+
}
|
|
1951
|
+
},
|
|
1952
|
+
async run({ args }) {
|
|
1953
|
+
p8.intro(`${pc7.cyan("mcpman sync")}`);
|
|
1954
|
+
const sourceClient = args.source;
|
|
1955
|
+
if (sourceClient && !VALID_CLIENTS.includes(sourceClient)) {
|
|
1956
|
+
p8.log.error(`Invalid --source "${sourceClient}". Must be one of: ${VALID_CLIENTS.join(", ")}`);
|
|
1957
|
+
process.exit(1);
|
|
1958
|
+
}
|
|
1959
|
+
const spinner5 = p8.spinner();
|
|
1960
|
+
spinner5.start("Detecting clients and reading configs...");
|
|
1961
|
+
const { configs, handlers } = await getClientConfigs();
|
|
1962
|
+
spinner5.stop(`Found ${configs.size} client(s)`);
|
|
1963
|
+
if (configs.size === 0) {
|
|
1964
|
+
p8.log.warn("No AI clients detected. Install Claude Desktop, Cursor, VS Code, or Windsurf first.");
|
|
1965
|
+
process.exit(0);
|
|
1966
|
+
}
|
|
1967
|
+
const diffOptions = { remove: args.remove };
|
|
1968
|
+
let actions;
|
|
1969
|
+
if (sourceClient) {
|
|
1970
|
+
if (!configs.has(sourceClient)) {
|
|
1971
|
+
p8.log.error(`Source client "${sourceClient}" is not detected or its config is unreadable.`);
|
|
1972
|
+
process.exit(1);
|
|
1973
|
+
}
|
|
1974
|
+
p8.log.info(`Using ${CLIENT_DISPLAY3[sourceClient]} as source of truth`);
|
|
1975
|
+
actions = computeDiffFromClient(sourceClient, configs, diffOptions);
|
|
1976
|
+
} else {
|
|
1977
|
+
const lockfile = readLockfile();
|
|
1978
|
+
actions = computeDiff(lockfile, configs, diffOptions);
|
|
1979
|
+
}
|
|
1980
|
+
printDiffTable(actions);
|
|
1981
|
+
const addCount = actions.filter((a) => a.action === "add").length;
|
|
1982
|
+
const extraCount = actions.filter((a) => a.action === "extra").length;
|
|
1983
|
+
const removeCount = actions.filter((a) => a.action === "remove").length;
|
|
1984
|
+
if (addCount === 0 && removeCount === 0 && extraCount === 0) {
|
|
1985
|
+
p8.outro(pc7.green("All clients are in sync."));
|
|
1986
|
+
process.exit(0);
|
|
1987
|
+
}
|
|
1988
|
+
const parts = [];
|
|
1989
|
+
if (addCount > 0) parts.push(pc7.green(`${addCount} to add`));
|
|
1990
|
+
if (removeCount > 0) parts.push(pc7.red(`${removeCount} to remove`));
|
|
1991
|
+
if (extraCount > 0) parts.push(pc7.yellow(`${extraCount} extra (informational)`));
|
|
1992
|
+
p8.log.info(parts.join(" \xB7 "));
|
|
1993
|
+
if (args["dry-run"]) {
|
|
1994
|
+
p8.outro(pc7.dim("Dry run \u2014 no changes applied."));
|
|
1995
|
+
process.exit(1);
|
|
1996
|
+
}
|
|
1997
|
+
if (addCount === 0 && removeCount === 0) {
|
|
1998
|
+
p8.outro(pc7.dim("No additions needed. Extra servers left untouched."));
|
|
1999
|
+
process.exit(1);
|
|
2000
|
+
}
|
|
2001
|
+
if (!args.yes) {
|
|
2002
|
+
const actionParts = [];
|
|
2003
|
+
if (addCount > 0) actionParts.push(`${addCount} addition(s)`);
|
|
2004
|
+
if (removeCount > 0) actionParts.push(`${removeCount} removal(s)`);
|
|
2005
|
+
const confirmed = await p8.confirm({
|
|
2006
|
+
message: `Apply ${actionParts.join(" and ")} to client configs?`,
|
|
2007
|
+
initialValue: true
|
|
2008
|
+
});
|
|
2009
|
+
if (p8.isCancel(confirmed) || !confirmed) {
|
|
2010
|
+
p8.outro(pc7.dim("Cancelled \u2014 no changes applied."));
|
|
2011
|
+
process.exit(0);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
spinner5.start("Applying sync changes...");
|
|
2015
|
+
const result = await applySyncActions(actions, handlers);
|
|
2016
|
+
spinner5.stop("Done");
|
|
2017
|
+
if (result.applied > 0) {
|
|
2018
|
+
p8.log.success(`Added ${result.applied} server(s) to client configs.`);
|
|
2019
|
+
}
|
|
2020
|
+
if (result.removed > 0) {
|
|
2021
|
+
p8.log.success(`Removed ${result.removed} server(s) from client configs.`);
|
|
2022
|
+
}
|
|
2023
|
+
if (result.failed > 0) {
|
|
2024
|
+
for (const e of result.errors) {
|
|
2025
|
+
p8.log.error(`Failed to sync "${e.server}" on ${e.client}: ${e.error}`);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
p8.outro(result.failed === 0 ? pc7.green("Sync complete.") : pc7.yellow("Sync complete with errors."));
|
|
2029
|
+
process.exit(result.failed > 0 ? 1 : 0);
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
2032
|
+
function printDiffTable(actions) {
|
|
2033
|
+
if (actions.length === 0) {
|
|
2034
|
+
p8.log.info("No actions to display.");
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
const nameWidth = Math.max(6, ...actions.map((a) => a.server.length));
|
|
2038
|
+
const clientWidth = Math.max(6, ...actions.map((a) => CLIENT_DISPLAY3[a.client]?.length ?? a.client.length));
|
|
2039
|
+
const header = ` ${pad2("SERVER", nameWidth)} ${pad2("CLIENT", clientWidth)} STATUS`;
|
|
2040
|
+
console.log(pc7.dim(header));
|
|
2041
|
+
console.log(pc7.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientWidth)} ------`));
|
|
2042
|
+
for (const action of actions) {
|
|
2043
|
+
const clientDisplay = CLIENT_DISPLAY3[action.client] ?? action.client;
|
|
2044
|
+
const [icon, statusText] = formatAction(action.action);
|
|
2045
|
+
console.log(` ${pad2(action.server, nameWidth)} ${pad2(clientDisplay, clientWidth)} ${icon} ${statusText}`);
|
|
2046
|
+
}
|
|
2047
|
+
console.log("");
|
|
2048
|
+
}
|
|
2049
|
+
function formatAction(action) {
|
|
2050
|
+
switch (action) {
|
|
2051
|
+
case "add":
|
|
2052
|
+
return [pc7.green("+"), pc7.green("missing \u2014 will add")];
|
|
2053
|
+
case "extra":
|
|
2054
|
+
return [pc7.yellow("?"), pc7.yellow("extra (not in lockfile)")];
|
|
2055
|
+
case "remove":
|
|
2056
|
+
return [pc7.red("\u2013"), pc7.red("extra \u2014 will remove")];
|
|
2057
|
+
case "ok":
|
|
2058
|
+
return [pc7.dim("\xB7"), pc7.dim("in sync")];
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
function pad2(s, width) {
|
|
2062
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// src/commands/update.ts
|
|
2066
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
2067
|
+
import * as p9 from "@clack/prompts";
|
|
2068
|
+
import pc9 from "picocolors";
|
|
2069
|
+
|
|
2070
|
+
// src/core/update-notifier.ts
|
|
2071
|
+
import fs3 from "fs";
|
|
2072
|
+
import path4 from "path";
|
|
2073
|
+
import os3 from "os";
|
|
2074
|
+
import pc8 from "picocolors";
|
|
2075
|
+
var CACHE_FILE = path4.join(os3.homedir(), ".mcpman", ".update-check");
|
|
2076
|
+
var TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2077
|
+
function writeUpdateCache(data) {
|
|
2078
|
+
try {
|
|
2079
|
+
const dir = path4.dirname(CACHE_FILE);
|
|
2080
|
+
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
2081
|
+
const tmp = `${CACHE_FILE}.tmp`;
|
|
2082
|
+
fs3.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
2083
|
+
fs3.renameSync(tmp, CACHE_FILE);
|
|
2084
|
+
} catch {
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// src/commands/update.ts
|
|
2089
|
+
async function loadClients3() {
|
|
2090
|
+
try {
|
|
2091
|
+
const mod = await import("./client-detector-SUIJSIYM.js");
|
|
2092
|
+
return mod.getInstalledClients();
|
|
2093
|
+
} catch {
|
|
2094
|
+
return [];
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
function printTable(updates) {
|
|
2098
|
+
const NAME_W = 28;
|
|
2099
|
+
const VER_W = 10;
|
|
2100
|
+
const header = [
|
|
2101
|
+
"NAME".padEnd(NAME_W),
|
|
2102
|
+
"CURRENT".padEnd(VER_W),
|
|
2103
|
+
"LATEST".padEnd(VER_W),
|
|
2104
|
+
"STATUS"
|
|
2105
|
+
].join(" ");
|
|
2106
|
+
console.log(pc9.bold(`
|
|
2107
|
+
${header}`));
|
|
2108
|
+
console.log(pc9.dim(` ${"\u2500".repeat(NAME_W + VER_W * 2 + 20)}`));
|
|
2109
|
+
for (const u of updates) {
|
|
2110
|
+
const nameCol = u.server.slice(0, NAME_W).padEnd(NAME_W);
|
|
2111
|
+
const curCol = u.currentVersion.padEnd(VER_W);
|
|
2112
|
+
const latCol = u.latestVersion.padEnd(VER_W);
|
|
2113
|
+
const statusCol = u.hasUpdate ? pc9.yellow(`Update available${u.updateType ? ` [${u.updateType}]` : ""}`) : pc9.green("Up to date");
|
|
2114
|
+
console.log(` ${nameCol} ${curCol} ${latCol} ${statusCol}`);
|
|
2115
|
+
}
|
|
2116
|
+
console.log();
|
|
2117
|
+
}
|
|
2118
|
+
var update_default = defineCommand9({
|
|
2119
|
+
meta: {
|
|
2120
|
+
name: "update",
|
|
2121
|
+
description: "Check for and apply updates to installed MCP servers"
|
|
2122
|
+
},
|
|
2123
|
+
args: {
|
|
2124
|
+
server: {
|
|
2125
|
+
type: "positional",
|
|
2126
|
+
description: "Server name to update (omit to update all)",
|
|
2127
|
+
required: false
|
|
2128
|
+
},
|
|
2129
|
+
check: {
|
|
2130
|
+
type: "boolean",
|
|
2131
|
+
description: "Check only \u2014 do not apply updates",
|
|
2132
|
+
default: false
|
|
2133
|
+
},
|
|
2134
|
+
yes: {
|
|
2135
|
+
type: "boolean",
|
|
2136
|
+
description: "Skip confirmation prompt",
|
|
2137
|
+
default: false
|
|
2138
|
+
},
|
|
2139
|
+
json: {
|
|
2140
|
+
type: "boolean",
|
|
2141
|
+
description: "Output results as JSON",
|
|
2142
|
+
default: false
|
|
2143
|
+
}
|
|
2144
|
+
},
|
|
2145
|
+
async run({ args }) {
|
|
2146
|
+
const lockfile = readLockfile();
|
|
2147
|
+
const servers = lockfile.servers;
|
|
2148
|
+
const targetEntries = args.server ? Object.entries(servers).filter(([name]) => name === args.server) : Object.entries(servers);
|
|
2149
|
+
if (targetEntries.length === 0) {
|
|
2150
|
+
if (args.server) {
|
|
2151
|
+
console.error(`Server '${args.server}' not found in lockfile.`);
|
|
2152
|
+
} else {
|
|
2153
|
+
console.log("No servers installed. Run mcpman install <server> first.");
|
|
2154
|
+
}
|
|
2155
|
+
process.exit(1);
|
|
2156
|
+
}
|
|
2157
|
+
const spinner5 = p9.spinner();
|
|
2158
|
+
spinner5.start("Checking versions...");
|
|
2159
|
+
let updates;
|
|
2160
|
+
try {
|
|
2161
|
+
const partialLock = {
|
|
2162
|
+
lockfileVersion: 1,
|
|
2163
|
+
servers: Object.fromEntries(targetEntries)
|
|
2164
|
+
};
|
|
2165
|
+
updates = await checkAllVersions(partialLock);
|
|
2166
|
+
} catch (err) {
|
|
2167
|
+
spinner5.stop("Version check failed");
|
|
2168
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1047
2169
|
process.exit(1);
|
|
1048
2170
|
}
|
|
1049
|
-
|
|
2171
|
+
spinner5.stop(`Checked ${updates.length} server(s)`);
|
|
2172
|
+
if (args.json) {
|
|
2173
|
+
console.log(JSON.stringify(updates, null, 2));
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
printTable(updates);
|
|
2177
|
+
const outdated = updates.filter((u) => u.hasUpdate);
|
|
2178
|
+
if (outdated.length === 0) {
|
|
2179
|
+
console.log(pc9.green(" All servers are up to date."));
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
if (args.check) {
|
|
2183
|
+
console.log(pc9.yellow(` ${outdated.length} update(s) available. Run mcpman update to apply.`));
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
if (!args.yes) {
|
|
2187
|
+
const confirmed = await p9.confirm({
|
|
2188
|
+
message: `Apply ${outdated.length} update(s)?`,
|
|
2189
|
+
initialValue: true
|
|
2190
|
+
});
|
|
2191
|
+
if (p9.isCancel(confirmed) || !confirmed) {
|
|
2192
|
+
p9.outro("Cancelled.");
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
const clients = await loadClients3();
|
|
2197
|
+
let successCount = 0;
|
|
2198
|
+
for (const update of outdated) {
|
|
2199
|
+
const s = p9.spinner();
|
|
2200
|
+
s.start(`Updating ${update.server}...`);
|
|
2201
|
+
const result = await applyServerUpdate(
|
|
2202
|
+
update.server,
|
|
2203
|
+
servers[update.server],
|
|
2204
|
+
clients
|
|
2205
|
+
);
|
|
2206
|
+
if (result.success) {
|
|
2207
|
+
s.stop(`${pc9.green("\u2713")} ${update.server}: ${result.fromVersion} \u2192 ${result.toVersion}`);
|
|
2208
|
+
successCount++;
|
|
2209
|
+
} else {
|
|
2210
|
+
s.stop(`${pc9.red("\u2717")} ${update.server}: ${result.error}`);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
const freshLockfile = readLockfile(resolveLockfilePath());
|
|
2214
|
+
const freshUpdates = await checkAllVersions(freshLockfile);
|
|
2215
|
+
writeUpdateCache({ lastCheck: (/* @__PURE__ */ new Date()).toISOString(), updates: freshUpdates });
|
|
2216
|
+
p9.outro(`${successCount} of ${outdated.length} server(s) updated.`);
|
|
1050
2217
|
}
|
|
1051
2218
|
});
|
|
1052
2219
|
|
|
1053
2220
|
// src/utils/constants.ts
|
|
1054
2221
|
var APP_NAME = "mcpman";
|
|
1055
|
-
var APP_VERSION = "0.
|
|
2222
|
+
var APP_VERSION = "0.3.0";
|
|
1056
2223
|
var APP_DESCRIPTION = "The package manager for MCP servers";
|
|
1057
2224
|
|
|
1058
2225
|
// src/index.ts
|
|
@@ -1060,7 +2227,7 @@ process.on("SIGINT", () => {
|
|
|
1060
2227
|
console.log("\nAborted.");
|
|
1061
2228
|
process.exit(130);
|
|
1062
2229
|
});
|
|
1063
|
-
var main =
|
|
2230
|
+
var main = defineCommand10({
|
|
1064
2231
|
meta: {
|
|
1065
2232
|
name: APP_NAME,
|
|
1066
2233
|
version: APP_VERSION,
|
|
@@ -1071,7 +2238,11 @@ var main = defineCommand6({
|
|
|
1071
2238
|
list: list_default,
|
|
1072
2239
|
remove: remove_default,
|
|
1073
2240
|
doctor: doctor_default,
|
|
1074
|
-
init: init_default
|
|
2241
|
+
init: init_default,
|
|
2242
|
+
secrets: secrets_default,
|
|
2243
|
+
sync: sync_default,
|
|
2244
|
+
audit: audit_default,
|
|
2245
|
+
update: update_default
|
|
1075
2246
|
}
|
|
1076
2247
|
});
|
|
1077
2248
|
runMain(main);
|