mcpman 0.2.0 → 0.4.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/dist/index.cjs CHANGED
@@ -159,9 +159,9 @@ async function atomicWrite(filePath, content) {
159
159
  throw err;
160
160
  }
161
161
  }
162
- async function pathExists(p9) {
162
+ async function pathExists(p12) {
163
163
  try {
164
- await import_node_fs3.default.promises.access(p9);
164
+ await import_node_fs3.default.promises.access(p12);
165
165
  return true;
166
166
  } catch {
167
167
  return false;
@@ -237,6 +237,12 @@ var init_base_client_handler = __esm({
237
237
  function getHomedir() {
238
238
  return import_node_os3.default.homedir();
239
239
  }
240
+ function getMcpmanDir() {
241
+ return import_node_path4.default.join(import_node_os3.default.homedir(), ".mcpman");
242
+ }
243
+ function getConfigPath() {
244
+ return import_node_path4.default.join(getMcpmanDir(), "config.json");
245
+ }
240
246
  function getAppDataDir() {
241
247
  const home = getHomedir();
242
248
  if (process.platform === "darwin") {
@@ -432,12 +438,12 @@ __export(vault_service_exports, {
432
438
  writeVault: () => writeVault
433
439
  });
434
440
  function getVaultPath() {
435
- return import_node_path6.default.join(import_node_os4.default.homedir(), ".mcpman", "vault.enc");
441
+ return import_node_path7.default.join(import_node_os4.default.homedir(), ".mcpman", "vault.enc");
436
442
  }
437
443
  function readVault(vaultPath = getVaultPath()) {
438
444
  const empty = { version: 1, servers: {} };
439
445
  try {
440
- const raw = import_node_fs4.default.readFileSync(vaultPath, "utf-8");
446
+ const raw = import_node_fs5.default.readFileSync(vaultPath, "utf-8");
441
447
  const parsed = JSON.parse(raw);
442
448
  if (parsed.version !== 1 || typeof parsed.servers !== "object") return empty;
443
449
  return parsed;
@@ -446,16 +452,16 @@ function readVault(vaultPath = getVaultPath()) {
446
452
  }
447
453
  }
448
454
  function writeVault(data, vaultPath = getVaultPath()) {
449
- const dir = import_node_path6.default.dirname(vaultPath);
450
- import_node_fs4.default.mkdirSync(dir, { recursive: true });
455
+ const dir = import_node_path7.default.dirname(vaultPath);
456
+ import_node_fs5.default.mkdirSync(dir, { recursive: true });
451
457
  const tmp = `${vaultPath}.tmp`;
452
- import_node_fs4.default.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
458
+ import_node_fs5.default.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
453
459
  if (process.platform !== "win32") {
454
- import_node_fs4.default.chmodSync(tmp, 384);
460
+ import_node_fs5.default.chmodSync(tmp, 384);
455
461
  }
456
- import_node_fs4.default.renameSync(tmp, vaultPath);
462
+ import_node_fs5.default.renameSync(tmp, vaultPath);
457
463
  if (process.platform !== "win32") {
458
- import_node_fs4.default.chmodSync(vaultPath, 384);
464
+ import_node_fs5.default.chmodSync(vaultPath, 384);
459
465
  }
460
466
  }
461
467
  function encrypt(value, password2) {
@@ -482,20 +488,20 @@ function decrypt(entry, password2) {
482
488
  ]);
483
489
  return decrypted.toString("utf-8");
484
490
  }
485
- async function getMasterPassword(confirm6 = false) {
491
+ async function getMasterPassword(confirm9 = false) {
486
492
  if (_cachedPassword) return _cachedPassword;
487
- const password2 = await p5.password({
493
+ const password2 = await p4.password({
488
494
  message: "Enter vault master password:",
489
495
  validate: (v) => v.length < 8 ? "Password must be at least 8 characters" : void 0
490
496
  });
491
- if (p5.isCancel(password2)) {
492
- p5.cancel("Vault access cancelled.");
497
+ if (p4.isCancel(password2)) {
498
+ p4.cancel("Vault access cancelled.");
493
499
  process.exit(0);
494
500
  }
495
- if (confirm6) {
496
- const confirm22 = await p5.password({ message: "Confirm master password:" });
497
- if (p5.isCancel(confirm22) || confirm22 !== password2) {
498
- p5.cancel("Passwords do not match.");
501
+ if (confirm9) {
502
+ const confirm22 = await p4.password({ message: "Confirm master password:" });
503
+ if (p4.isCancel(confirm22) || confirm22 !== password2) {
504
+ p4.cancel("Passwords do not match.");
499
505
  process.exit(1);
500
506
  }
501
507
  }
@@ -545,16 +551,16 @@ function listSecrets(server, vaultPath = getVaultPath()) {
545
551
  keys: Object.keys(keys)
546
552
  }));
547
553
  }
548
- var import_node_crypto2, import_node_fs4, import_node_os4, import_node_path6, p5, _cachedPassword;
554
+ var import_node_crypto2, import_node_fs5, import_node_os4, import_node_path7, p4, _cachedPassword;
549
555
  var init_vault_service = __esm({
550
556
  "src/core/vault-service.ts"() {
551
557
  "use strict";
552
558
  init_cjs_shims();
553
559
  import_node_crypto2 = __toESM(require("crypto"), 1);
554
- import_node_fs4 = __toESM(require("fs"), 1);
560
+ import_node_fs5 = __toESM(require("fs"), 1);
555
561
  import_node_os4 = __toESM(require("os"), 1);
556
- import_node_path6 = __toESM(require("path"), 1);
557
- p5 = __toESM(require("@clack/prompts"), 1);
562
+ import_node_path7 = __toESM(require("path"), 1);
563
+ p4 = __toESM(require("@clack/prompts"), 1);
558
564
  _cachedPassword = null;
559
565
  process.on("exit", () => {
560
566
  _cachedPassword = null;
@@ -564,11 +570,12 @@ var init_vault_service = __esm({
564
570
 
565
571
  // src/index.ts
566
572
  init_cjs_shims();
567
- var import_citty10 = require("citty");
573
+ var import_citty14 = require("citty");
568
574
 
569
575
  // src/commands/audit.ts
570
576
  init_cjs_shims();
571
577
  var import_citty = require("citty");
578
+ var p = __toESM(require("@clack/prompts"), 1);
572
579
  var import_picocolors = __toESM(require("picocolors"), 1);
573
580
  var import_nanospinner = require("nanospinner");
574
581
 
@@ -766,181 +773,744 @@ async function scanAllServers(servers, concurrency = 3) {
766
773
  const results = [];
767
774
  const executing = /* @__PURE__ */ new Set();
768
775
  for (const [name, entry] of entries) {
769
- const p9 = scanServer(name, entry).then((r) => {
776
+ const p12 = scanServer(name, entry).then((r) => {
770
777
  results.push(r);
771
- executing.delete(p9);
778
+ executing.delete(p12);
772
779
  });
773
- executing.add(p9);
780
+ executing.add(p12);
774
781
  if (executing.size >= concurrency) await Promise.race(executing);
775
782
  }
776
783
  await Promise.all(executing);
777
784
  return results;
778
785
  }
779
786
 
780
- // src/commands/audit.ts
781
- function colorRisk(level, score) {
782
- const label = score !== null ? `${score}/100 (${level})` : level;
783
- if (level === "LOW") return import_picocolors.default.green(label);
784
- if (level === "MEDIUM") return import_picocolors.default.yellow(label);
785
- if (level === "HIGH") return import_picocolors.default.red(label);
786
- if (level === "CRITICAL") return import_picocolors.default.bold(import_picocolors.default.red(label));
787
- return import_picocolors.default.dim(label);
788
- }
789
- function daysAgo(isoDate) {
790
- const days = Math.floor((Date.now() - new Date(isoDate).getTime()) / 864e5);
791
- if (days === 0) return "today";
792
- if (days === 1) return "1 day ago";
793
- return `${days} days ago`;
787
+ // src/core/version-checker.ts
788
+ init_cjs_shims();
789
+ function compareVersions(a, b) {
790
+ const aParts = a.replace(/^v/, "").split(".").map(Number);
791
+ const bParts = b.replace(/^v/, "").split(".").map(Number);
792
+ const len = Math.max(aParts.length, bParts.length);
793
+ for (let i = 0; i < len; i++) {
794
+ const aN = aParts[i] ?? 0;
795
+ const bN = bParts[i] ?? 0;
796
+ if (Number.isNaN(aN) || Number.isNaN(bN)) return 0;
797
+ if (aN < bN) return -1;
798
+ if (aN > bN) return 1;
799
+ }
800
+ return 0;
794
801
  }
795
- function countVulns(vulns) {
796
- const c = { critical: 0, high: 0, moderate: 0, low: 0 };
797
- for (const v of vulns) c[v.severity]++;
798
- if (vulns.length === 0) return import_picocolors.default.green("none");
799
- const parts = [];
800
- if (c.critical) parts.push(import_picocolors.default.bold(import_picocolors.default.red(`${c.critical} critical`)));
801
- if (c.high) parts.push(import_picocolors.default.red(`${c.high} high`));
802
- if (c.moderate) parts.push(import_picocolors.default.yellow(`${c.moderate} moderate`));
803
- if (c.low) parts.push(import_picocolors.default.dim(`${c.low} low`));
804
- return parts.join(", ");
802
+ function detectUpdateType(current, latest) {
803
+ const cParts = current.replace(/^v/, "").split(".").map(Number);
804
+ const lParts = latest.replace(/^v/, "").split(".").map(Number);
805
+ if ((lParts[0] ?? 0) > (cParts[0] ?? 0)) return "major";
806
+ if ((lParts[1] ?? 0) > (cParts[1] ?? 0)) return "minor";
807
+ return "patch";
805
808
  }
806
- function printReport(report) {
807
- const riskColored = colorRisk(report.riskLevel, report.score);
808
- const icon = report.riskLevel === "LOW" ? import_picocolors.default.green("\u25CF") : report.riskLevel === "MEDIUM" ? import_picocolors.default.yellow("\u25CF") : report.riskLevel === "UNKNOWN" ? import_picocolors.default.dim("\u25CB") : import_picocolors.default.red("\u25CF");
809
- console.log(` ${icon} ${import_picocolors.default.bold(report.server)} Score: ${riskColored}`);
810
- if (report.source !== "npm") {
811
- console.log(` ${import_picocolors.default.dim("Non-npm source \u2014 security data unavailable")}`);
812
- console.log();
813
- return;
814
- }
815
- if (report.metadata) {
816
- const { weeklyDownloads, packageAge, lastPublish, maintainerCount, deprecated } = report.metadata;
817
- const dlStr = weeklyDownloads.toLocaleString();
818
- console.log(
819
- ` ${import_picocolors.default.dim("Downloads:")} ${dlStr}/week ${import_picocolors.default.dim("|")} ${import_picocolors.default.dim("Age:")} ${packageAge}d ${import_picocolors.default.dim("|")} ${import_picocolors.default.dim("Last publish:")} ${daysAgo(lastPublish)} ${import_picocolors.default.dim("|")} ${import_picocolors.default.dim("Maintainers:")} ${maintainerCount}` + (deprecated ? import_picocolors.default.red(" [DEPRECATED]") : "")
809
+ async function fetchNpmLatest(packageName) {
810
+ try {
811
+ const res = await fetch(
812
+ `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
813
+ {
814
+ headers: { Accept: "application/json" },
815
+ signal: AbortSignal.timeout(8e3)
816
+ }
820
817
  );
818
+ if (!res.ok) return null;
819
+ const data = await res.json();
820
+ return typeof data.version === "string" ? data.version : null;
821
+ } catch {
822
+ return null;
821
823
  }
822
- console.log(` ${import_picocolors.default.dim("Vulnerabilities:")} ${countVulns(report.vulnerabilities)}`);
823
- if (report.vulnerabilities.length > 0) {
824
- for (const v of report.vulnerabilities) {
825
- const sevColor = v.severity === "critical" || v.severity === "high" ? import_picocolors.default.red : import_picocolors.default.yellow;
826
- const url = v.url ? import_picocolors.default.dim(` ${v.url}`) : "";
827
- console.log(` ${sevColor("\u25B8")} [${v.severity}] ${v.title}${url}`);
828
- }
829
- }
830
- console.log();
831
824
  }
832
- var audit_default = (0, import_citty.defineCommand)({
833
- meta: {
834
- name: "audit",
835
- description: "Scan installed MCP servers for security vulnerabilities and trust scores"
836
- },
837
- args: {
838
- server: {
839
- type: "positional",
840
- description: "Specific server to audit (omit to audit all)",
841
- required: false
842
- },
843
- json: {
844
- type: "boolean",
845
- description: "Output results as JSON",
846
- default: false
847
- },
848
- fix: {
849
- type: "boolean",
850
- description: "Show available fix versions for vulnerable packages",
851
- default: false
852
- }
853
- },
854
- async run({ args }) {
855
- const lockfile = readLockfile();
856
- const { servers } = lockfile;
857
- if (Object.keys(servers).length === 0) {
858
- console.log(import_picocolors.default.dim("\n No MCP servers installed. Run mcpman install <server> to get started.\n"));
859
- return;
860
- }
861
- const targets = {};
862
- if (args.server) {
863
- if (!servers[args.server]) {
864
- console.error(import_picocolors.default.red(`
865
- Server "${args.server}" not found in lockfile.
866
- `));
867
- process.exit(1);
825
+ async function fetchSmitheryLatest(name) {
826
+ try {
827
+ const res = await fetch(
828
+ `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`,
829
+ {
830
+ headers: { Accept: "application/json" },
831
+ signal: AbortSignal.timeout(8e3)
868
832
  }
869
- targets[args.server] = servers[args.server];
870
- } else {
871
- Object.assign(targets, servers);
872
- }
873
- const spinner5 = (0, import_nanospinner.createSpinner)(`Scanning ${Object.keys(targets).length} server(s)...`).start();
874
- let reports;
875
- try {
876
- reports = args.server ? [await scanServer(args.server, targets[args.server])] : await scanAllServers(targets);
877
- } catch (err) {
878
- spinner5.error({ text: "Scan failed" });
879
- console.error(import_picocolors.default.red(String(err)));
880
- process.exit(1);
881
- }
882
- spinner5.success({ text: `Scanned ${reports.length} server(s)` });
883
- if (args.json) {
884
- console.log(JSON.stringify(reports, null, 2));
885
- return;
886
- }
887
- console.log(import_picocolors.default.bold("\n mcpman audit\n"));
888
- console.log(import_picocolors.default.dim(" " + "\u2500".repeat(60)));
889
- for (const report of reports) {
890
- printReport(report);
891
- }
892
- console.log(import_picocolors.default.dim(" " + "\u2500".repeat(60)));
893
- const withIssues = reports.filter(
894
- (r) => r.riskLevel !== "LOW" && r.riskLevel !== "UNKNOWN"
895
833
  );
896
- const npmReports = reports.filter((r) => r.source === "npm");
897
- const parts = [];
898
- parts.push(`${reports.length} server(s) scanned`);
899
- if (npmReports.length < reports.length) {
900
- parts.push(import_picocolors.default.dim(`${reports.length - npmReports.length} non-npm (unverified)`));
901
- }
902
- if (withIssues.length > 0) {
903
- parts.push(import_picocolors.default.yellow(`${withIssues.length} with issues`));
904
- } else {
905
- parts.push(import_picocolors.default.green("all clear"));
906
- }
907
- console.log(`
908
- Summary: ${parts.join(" | ")}
909
- `);
910
- if (args.fix) {
911
- const withVulns = reports.filter((r) => r.vulnerabilities.length > 0);
912
- if (withVulns.length > 0) {
913
- console.log(import_picocolors.default.bold(" Fix suggestions:"));
914
- for (const r of withVulns) {
915
- console.log(` ${import_picocolors.default.cyan("\u2192")} Run ${import_picocolors.default.cyan(`mcpman install ${r.server}@latest`)} to update`);
916
- }
917
- console.log();
834
+ if (!res.ok) return null;
835
+ const data = await res.json();
836
+ return typeof data.version === "string" ? data.version : null;
837
+ } catch {
838
+ return null;
839
+ }
840
+ }
841
+ async function fetchGithubLatest(resolved) {
842
+ const match = resolved.match(/github\.com\/([^/]+)\/([^/]+)/);
843
+ if (!match) return null;
844
+ const [, owner, repo] = match;
845
+ try {
846
+ const res = await fetch(
847
+ `https://api.github.com/repos/${owner}/${repo}/releases/latest`,
848
+ {
849
+ headers: { Accept: "application/json" },
850
+ signal: AbortSignal.timeout(8e3)
918
851
  }
852
+ );
853
+ if (!res.ok) return null;
854
+ const data = await res.json();
855
+ return typeof data.tag_name === "string" ? data.tag_name.replace(/^v/, "") : null;
856
+ } catch {
857
+ return null;
858
+ }
859
+ }
860
+ async function checkVersion(name, lockEntry) {
861
+ const current = lockEntry.version;
862
+ let latest = null;
863
+ if (lockEntry.source === "npm") {
864
+ latest = await fetchNpmLatest(name);
865
+ } else if (lockEntry.source === "smithery") {
866
+ latest = await fetchSmitheryLatest(name);
867
+ } else if (lockEntry.source === "github") {
868
+ latest = await fetchGithubLatest(lockEntry.resolved);
869
+ }
870
+ if (!latest || latest === current) {
871
+ return {
872
+ server: name,
873
+ source: lockEntry.source,
874
+ currentVersion: current,
875
+ latestVersion: latest ?? current,
876
+ hasUpdate: false
877
+ };
878
+ }
879
+ const hasUpdate = compareVersions(current, latest) === -1;
880
+ return {
881
+ server: name,
882
+ source: lockEntry.source,
883
+ currentVersion: current,
884
+ latestVersion: latest,
885
+ hasUpdate,
886
+ updateType: hasUpdate ? detectUpdateType(current, latest) : void 0
887
+ };
888
+ }
889
+ async function checkAllVersions(lockfile) {
890
+ const entries = Object.entries(lockfile.servers);
891
+ if (entries.length === 0) return [];
892
+ const results = [];
893
+ const executing = /* @__PURE__ */ new Set();
894
+ for (const [name, entry] of entries) {
895
+ const p12 = checkVersion(name, entry).then((r) => {
896
+ results.push(r);
897
+ executing.delete(p12);
898
+ });
899
+ executing.add(p12);
900
+ if (executing.size >= 5) {
901
+ await Promise.race(executing);
919
902
  }
920
903
  }
921
- });
904
+ await Promise.all(executing);
905
+ return results;
906
+ }
922
907
 
923
- // src/commands/doctor.ts
908
+ // src/core/server-updater.ts
924
909
  init_cjs_shims();
925
- var import_citty2 = require("citty");
926
- var import_picocolors2 = __toESM(require("picocolors"), 1);
927
910
 
928
- // src/core/server-inventory.ts
911
+ // src/core/server-resolver.ts
929
912
  init_cjs_shims();
930
- init_client_detector();
931
- async function getInstalledServers(clientFilter) {
932
- const clients = await getInstalledClients();
933
- const filtered = clientFilter ? clients.filter((c) => c.type === clientFilter) : clients;
934
- const serverMap = /* @__PURE__ */ new Map();
935
- for (const client of filtered) {
936
- let config;
937
- try {
938
- config = await client.readConfig();
939
- } catch {
940
- continue;
941
- }
942
- for (const [name, entry] of Object.entries(config.servers)) {
943
- const existing = serverMap.get(name);
913
+
914
+ // src/core/registry.ts
915
+ init_cjs_shims();
916
+ var import_node_crypto = require("crypto");
917
+ function computeIntegrity(resolvedUrl) {
918
+ const hash = (0, import_node_crypto.createHash)("sha512").update(resolvedUrl).digest("base64");
919
+ return `sha512-${hash}`;
920
+ }
921
+ async function resolveFromSmithery(name) {
922
+ const url = `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`;
923
+ let data;
924
+ try {
925
+ const res = await fetch(url, {
926
+ headers: { Accept: "application/json" },
927
+ signal: AbortSignal.timeout(8e3)
928
+ });
929
+ if (res.status === 404) {
930
+ throw new Error(`Server '${name}' not found on Smithery registry`);
931
+ }
932
+ if (!res.ok) {
933
+ throw new Error(`Smithery API error: ${res.status}`);
934
+ }
935
+ data = await res.json();
936
+ } catch (err) {
937
+ if (err instanceof Error && err.message.includes("not found")) throw err;
938
+ throw new Error(
939
+ `Cannot reach Smithery registry: ${err instanceof Error ? err.message : String(err)}`
940
+ );
941
+ }
942
+ const version = typeof data.version === "string" ? data.version : "latest";
943
+ const command = typeof data.command === "string" ? data.command : "npx";
944
+ const args = Array.isArray(data.args) ? data.args : ["-y", `${name}@${version}`];
945
+ const envVars = Array.isArray(data.envVars) ? data.envVars : [];
946
+ const resolved = typeof data.resolved === "string" ? data.resolved : `smithery:${name}@${version}`;
947
+ return {
948
+ name,
949
+ version,
950
+ description: typeof data.description === "string" ? data.description : "",
951
+ runtime: "node",
952
+ command,
953
+ args,
954
+ envVars,
955
+ resolved
956
+ };
957
+ }
958
+ async function resolveFromNpm(packageName) {
959
+ const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
960
+ let data;
961
+ try {
962
+ const res = await fetch(url, {
963
+ headers: { Accept: "application/json" },
964
+ signal: AbortSignal.timeout(8e3)
965
+ });
966
+ if (res.status === 404) {
967
+ throw new Error(`Package '${packageName}' not found on npm`);
968
+ }
969
+ if (!res.ok) {
970
+ throw new Error(`npm registry error: ${res.status}`);
971
+ }
972
+ data = await res.json();
973
+ } catch (err) {
974
+ if (err instanceof Error && err.message.includes("not found")) throw err;
975
+ throw new Error(
976
+ `Cannot reach npm registry: ${err instanceof Error ? err.message : String(err)}`
977
+ );
978
+ }
979
+ const version = typeof data.version === "string" ? data.version : "latest";
980
+ const resolved = `https://registry.npmjs.org/${packageName}/-/${packageName.replace(/^@[^/]+\//, "")}-${version}.tgz`;
981
+ const mcpField = data.mcp && typeof data.mcp === "object" ? data.mcp : null;
982
+ const envVars = mcpField?.envVars ? mcpField.envVars : [];
983
+ return {
984
+ name: packageName,
985
+ version,
986
+ description: typeof data.description === "string" ? data.description : "",
987
+ runtime: "node",
988
+ command: "npx",
989
+ args: ["-y", `${packageName}@${version}`],
990
+ envVars,
991
+ resolved
992
+ };
993
+ }
994
+ async function resolveFromGitHub(githubUrl) {
995
+ const match = githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
996
+ if (!match) {
997
+ throw new Error(`Invalid GitHub URL: ${githubUrl}`);
998
+ }
999
+ const [, owner, repo] = match;
1000
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/package.json`;
1001
+ let pkgData = {};
1002
+ try {
1003
+ const res = await fetch(rawUrl, { signal: AbortSignal.timeout(8e3) });
1004
+ if (res.ok) {
1005
+ pkgData = await res.json();
1006
+ }
1007
+ } catch {
1008
+ }
1009
+ const version = typeof pkgData.version === "string" ? pkgData.version : "main";
1010
+ const name = typeof pkgData.name === "string" ? pkgData.name : `${owner}/${repo}`;
1011
+ return {
1012
+ name,
1013
+ version,
1014
+ description: typeof pkgData.description === "string" ? pkgData.description : "",
1015
+ runtime: "node",
1016
+ command: "npx",
1017
+ args: ["-y", githubUrl],
1018
+ envVars: [],
1019
+ resolved: githubUrl
1020
+ };
1021
+ }
1022
+
1023
+ // src/core/server-resolver.ts
1024
+ function detectSource(input) {
1025
+ if (input.startsWith("smithery:")) {
1026
+ return { type: "smithery", input: input.slice(9) };
1027
+ }
1028
+ if (input.startsWith("https://github.com/") || input.startsWith("github.com/")) {
1029
+ return { type: "github", input };
1030
+ }
1031
+ return { type: "npm", input };
1032
+ }
1033
+ function parseEnvFlags(envFlags) {
1034
+ if (!envFlags) return {};
1035
+ const flags = Array.isArray(envFlags) ? envFlags : [envFlags];
1036
+ const result = {};
1037
+ for (const flag of flags) {
1038
+ const idx = flag.indexOf("=");
1039
+ if (idx > 0) {
1040
+ result[flag.slice(0, idx)] = flag.slice(idx + 1);
1041
+ }
1042
+ }
1043
+ return result;
1044
+ }
1045
+ async function resolveServer(input) {
1046
+ const source = detectSource(input);
1047
+ switch (source.type) {
1048
+ case "smithery":
1049
+ return resolveFromSmithery(source.input);
1050
+ case "github":
1051
+ return resolveFromGitHub(source.input);
1052
+ case "npm":
1053
+ return resolveFromNpm(source.input);
1054
+ }
1055
+ }
1056
+
1057
+ // src/core/server-updater.ts
1058
+ async function applyServerUpdate(serverName, lockEntry, clients) {
1059
+ const fromVersion = lockEntry.version;
1060
+ const input = lockEntry.source === "smithery" ? `smithery:${serverName}` : lockEntry.source === "github" ? lockEntry.resolved : serverName;
1061
+ try {
1062
+ const metadata = await resolveServer(input);
1063
+ const integrity = computeIntegrity(metadata.resolved);
1064
+ addEntry(serverName, {
1065
+ ...lockEntry,
1066
+ version: metadata.version,
1067
+ resolved: metadata.resolved,
1068
+ integrity,
1069
+ command: metadata.command,
1070
+ args: metadata.args,
1071
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
1072
+ });
1073
+ const targetClients = clients.filter(
1074
+ (c) => lockEntry.clients.includes(c.type)
1075
+ );
1076
+ for (const client of targetClients) {
1077
+ try {
1078
+ await client.addServer(serverName, {
1079
+ command: metadata.command,
1080
+ args: metadata.args
1081
+ });
1082
+ } catch {
1083
+ }
1084
+ }
1085
+ return {
1086
+ server: serverName,
1087
+ success: true,
1088
+ fromVersion,
1089
+ toVersion: metadata.version
1090
+ };
1091
+ } catch (err) {
1092
+ return {
1093
+ server: serverName,
1094
+ success: false,
1095
+ fromVersion,
1096
+ toVersion: fromVersion,
1097
+ error: err instanceof Error ? err.message : String(err)
1098
+ };
1099
+ }
1100
+ }
1101
+
1102
+ // src/commands/audit.ts
1103
+ function colorRisk(level, score) {
1104
+ const label = score !== null ? `${score}/100 (${level})` : level;
1105
+ if (level === "LOW") return import_picocolors.default.green(label);
1106
+ if (level === "MEDIUM") return import_picocolors.default.yellow(label);
1107
+ if (level === "HIGH") return import_picocolors.default.red(label);
1108
+ if (level === "CRITICAL") return import_picocolors.default.bold(import_picocolors.default.red(label));
1109
+ return import_picocolors.default.dim(label);
1110
+ }
1111
+ function daysAgo(isoDate) {
1112
+ const days = Math.floor((Date.now() - new Date(isoDate).getTime()) / 864e5);
1113
+ if (days === 0) return "today";
1114
+ if (days === 1) return "1 day ago";
1115
+ return `${days} days ago`;
1116
+ }
1117
+ function countVulns(vulns) {
1118
+ const c = { critical: 0, high: 0, moderate: 0, low: 0 };
1119
+ for (const v of vulns) c[v.severity]++;
1120
+ if (vulns.length === 0) return import_picocolors.default.green("none");
1121
+ const parts = [];
1122
+ if (c.critical) parts.push(import_picocolors.default.bold(import_picocolors.default.red(`${c.critical} critical`)));
1123
+ if (c.high) parts.push(import_picocolors.default.red(`${c.high} high`));
1124
+ if (c.moderate) parts.push(import_picocolors.default.yellow(`${c.moderate} moderate`));
1125
+ if (c.low) parts.push(import_picocolors.default.dim(`${c.low} low`));
1126
+ return parts.join(", ");
1127
+ }
1128
+ function printReport(report) {
1129
+ const riskColored = colorRisk(report.riskLevel, report.score);
1130
+ const icon = report.riskLevel === "LOW" ? import_picocolors.default.green("\u25CF") : report.riskLevel === "MEDIUM" ? import_picocolors.default.yellow("\u25CF") : report.riskLevel === "UNKNOWN" ? import_picocolors.default.dim("\u25CB") : import_picocolors.default.red("\u25CF");
1131
+ console.log(` ${icon} ${import_picocolors.default.bold(report.server)} Score: ${riskColored}`);
1132
+ if (report.source !== "npm") {
1133
+ console.log(` ${import_picocolors.default.dim("Non-npm source \u2014 security data unavailable")}`);
1134
+ console.log();
1135
+ return;
1136
+ }
1137
+ if (report.metadata) {
1138
+ const { weeklyDownloads, packageAge, lastPublish, maintainerCount, deprecated } = report.metadata;
1139
+ const dlStr = weeklyDownloads.toLocaleString();
1140
+ console.log(
1141
+ ` ${import_picocolors.default.dim("Downloads:")} ${dlStr}/week ${import_picocolors.default.dim("|")} ${import_picocolors.default.dim("Age:")} ${packageAge}d ${import_picocolors.default.dim("|")} ${import_picocolors.default.dim("Last publish:")} ${daysAgo(lastPublish)} ${import_picocolors.default.dim("|")} ${import_picocolors.default.dim("Maintainers:")} ${maintainerCount}` + (deprecated ? import_picocolors.default.red(" [DEPRECATED]") : "")
1142
+ );
1143
+ }
1144
+ console.log(` ${import_picocolors.default.dim("Vulnerabilities:")} ${countVulns(report.vulnerabilities)}`);
1145
+ if (report.vulnerabilities.length > 0) {
1146
+ for (const v of report.vulnerabilities) {
1147
+ const sevColor = v.severity === "critical" || v.severity === "high" ? import_picocolors.default.red : import_picocolors.default.yellow;
1148
+ const url = v.url ? import_picocolors.default.dim(` ${v.url}`) : "";
1149
+ console.log(` ${sevColor("\u25B8")} [${v.severity}] ${v.title}${url}`);
1150
+ }
1151
+ }
1152
+ console.log();
1153
+ }
1154
+ var audit_default = (0, import_citty.defineCommand)({
1155
+ meta: {
1156
+ name: "audit",
1157
+ description: "Scan installed MCP servers for security vulnerabilities and trust scores"
1158
+ },
1159
+ args: {
1160
+ server: {
1161
+ type: "positional",
1162
+ description: "Specific server to audit (omit to audit all)",
1163
+ required: false
1164
+ },
1165
+ json: {
1166
+ type: "boolean",
1167
+ description: "Output results as JSON",
1168
+ default: false
1169
+ },
1170
+ fix: {
1171
+ type: "boolean",
1172
+ description: "Apply updates to fix vulnerable packages",
1173
+ default: false
1174
+ },
1175
+ yes: {
1176
+ type: "boolean",
1177
+ description: "Skip confirmation prompt (use with --fix)",
1178
+ default: false
1179
+ }
1180
+ },
1181
+ async run({ args }) {
1182
+ const lockfile = readLockfile();
1183
+ const { servers } = lockfile;
1184
+ if (Object.keys(servers).length === 0) {
1185
+ console.log(import_picocolors.default.dim("\n No MCP servers installed. Run mcpman install <server> to get started.\n"));
1186
+ return;
1187
+ }
1188
+ const targets = {};
1189
+ if (args.server) {
1190
+ if (!servers[args.server]) {
1191
+ console.error(import_picocolors.default.red(`
1192
+ Server "${args.server}" not found in lockfile.
1193
+ `));
1194
+ process.exit(1);
1195
+ }
1196
+ targets[args.server] = servers[args.server];
1197
+ } else {
1198
+ Object.assign(targets, servers);
1199
+ }
1200
+ const spinner5 = (0, import_nanospinner.createSpinner)(`Scanning ${Object.keys(targets).length} server(s)...`).start();
1201
+ let reports;
1202
+ try {
1203
+ reports = args.server ? [await scanServer(args.server, targets[args.server])] : await scanAllServers(targets);
1204
+ } catch (err) {
1205
+ spinner5.error({ text: "Scan failed" });
1206
+ console.error(import_picocolors.default.red(String(err)));
1207
+ process.exit(1);
1208
+ }
1209
+ spinner5.success({ text: `Scanned ${reports.length} server(s)` });
1210
+ if (args.json) {
1211
+ console.log(JSON.stringify(reports, null, 2));
1212
+ return;
1213
+ }
1214
+ console.log(import_picocolors.default.bold("\n mcpman audit\n"));
1215
+ console.log(import_picocolors.default.dim(" " + "\u2500".repeat(60)));
1216
+ for (const report of reports) {
1217
+ printReport(report);
1218
+ }
1219
+ console.log(import_picocolors.default.dim(" " + "\u2500".repeat(60)));
1220
+ const withIssues = reports.filter(
1221
+ (r) => r.riskLevel !== "LOW" && r.riskLevel !== "UNKNOWN"
1222
+ );
1223
+ const npmReports = reports.filter((r) => r.source === "npm");
1224
+ const parts = [];
1225
+ parts.push(`${reports.length} server(s) scanned`);
1226
+ if (npmReports.length < reports.length) {
1227
+ parts.push(import_picocolors.default.dim(`${reports.length - npmReports.length} non-npm (unverified)`));
1228
+ }
1229
+ if (withIssues.length > 0) {
1230
+ parts.push(import_picocolors.default.yellow(`${withIssues.length} with issues`));
1231
+ } else {
1232
+ parts.push(import_picocolors.default.green("all clear"));
1233
+ }
1234
+ console.log(`
1235
+ Summary: ${parts.join(" | ")}
1236
+ `);
1237
+ if (args.fix) {
1238
+ await runAuditFix(reports, lockfile.servers, args.yes);
1239
+ }
1240
+ }
1241
+ });
1242
+ async function loadClients() {
1243
+ try {
1244
+ const mod = await Promise.resolve().then(() => (init_client_detector(), client_detector_exports));
1245
+ return mod.getInstalledClients();
1246
+ } catch {
1247
+ return [];
1248
+ }
1249
+ }
1250
+ async function runAuditFix(reports, servers, skipConfirm) {
1251
+ const npmWithVulns = reports.filter(
1252
+ (r) => r.vulnerabilities.length > 0 && r.source === "npm"
1253
+ );
1254
+ const nonNpmWithVulns = reports.filter(
1255
+ (r) => r.vulnerabilities.length > 0 && r.source !== "npm"
1256
+ );
1257
+ if (nonNpmWithVulns.length > 0) {
1258
+ console.log(import_picocolors.default.yellow(" Non-npm servers require manual update:"));
1259
+ for (const r of nonNpmWithVulns) {
1260
+ console.log(` ${import_picocolors.default.dim("\u2192")} ${r.server} (${r.source})`);
1261
+ }
1262
+ console.log();
1263
+ }
1264
+ if (npmWithVulns.length === 0) {
1265
+ console.log(import_picocolors.default.green(" No fixable vulnerabilities found.\n"));
1266
+ return;
1267
+ }
1268
+ const versionSpinner = (0, import_nanospinner.createSpinner)("Checking for available updates...").start();
1269
+ const versionChecks = await Promise.all(
1270
+ npmWithVulns.map((r) => checkVersion(r.server, servers[r.server]))
1271
+ );
1272
+ versionSpinner.success({ text: "Version check complete" });
1273
+ const updatable = versionChecks.filter((u) => u.hasUpdate);
1274
+ if (updatable.length === 0) {
1275
+ console.log(import_picocolors.default.yellow(
1276
+ " Vulnerable servers have no newer versions available yet.\n Allow time for registry to publish fixes.\n"
1277
+ ));
1278
+ return;
1279
+ }
1280
+ console.log(import_picocolors.default.bold(`
1281
+ ${updatable.length} server(s) can be updated to fix vulnerabilities:
1282
+ `));
1283
+ for (const u of updatable) {
1284
+ console.log(` ${import_picocolors.default.cyan("\u2192")} ${u.server} ${import_picocolors.default.dim(u.currentVersion)} \u2192 ${import_picocolors.default.green(u.latestVersion)}`);
1285
+ }
1286
+ console.log();
1287
+ if (!skipConfirm) {
1288
+ const confirmed = await p.confirm({
1289
+ message: `Update ${updatable.length} vulnerable server(s)?`,
1290
+ initialValue: true
1291
+ });
1292
+ if (p.isCancel(confirmed) || !confirmed) {
1293
+ p.outro("Cancelled.");
1294
+ return;
1295
+ }
1296
+ }
1297
+ const clients = await loadClients();
1298
+ let successCount = 0;
1299
+ const results = [];
1300
+ for (const u of updatable) {
1301
+ const s = (0, import_nanospinner.createSpinner)(`Updating ${u.server}...`).start();
1302
+ const result = await applyServerUpdate(u.server, servers[u.server], clients);
1303
+ if (result.success) {
1304
+ s.success({ text: `${import_picocolors.default.green("\u2713")} ${u.server}: ${result.fromVersion} \u2192 ${result.toVersion}` });
1305
+ successCount++;
1306
+ } else {
1307
+ s.error({ text: `${import_picocolors.default.red("\u2717")} ${u.server}: ${result.error}` });
1308
+ }
1309
+ results.push({
1310
+ server: u.server,
1311
+ from: result.fromVersion,
1312
+ to: result.toVersion,
1313
+ ok: result.success,
1314
+ error: result.error
1315
+ });
1316
+ }
1317
+ console.log();
1318
+ if (successCount > 0) {
1319
+ const updatedNames = results.filter((r) => r.ok).map((r) => r.server);
1320
+ const freshLockfile = readLockfile();
1321
+ const rescanSpinner = (0, import_nanospinner.createSpinner)("Re-scanning updated servers...").start();
1322
+ const afterReports = await Promise.all(
1323
+ updatedNames.map((name) => scanServer(name, freshLockfile.servers[name]))
1324
+ );
1325
+ rescanSpinner.success({ text: "Re-scan complete" });
1326
+ console.log(import_picocolors.default.bold("\n Before / After:\n"));
1327
+ for (const after of afterReports) {
1328
+ const before = reports.find((r) => r.server === after.server);
1329
+ const beforeVulns = before?.vulnerabilities.length ?? 0;
1330
+ const afterVulns = after.vulnerabilities.length;
1331
+ const improved = afterVulns < beforeVulns ? import_picocolors.default.green("improved") : import_picocolors.default.yellow("unchanged");
1332
+ console.log(
1333
+ ` ${import_picocolors.default.bold(after.server)} vulns: ${beforeVulns} \u2192 ${afterVulns} [${improved}]`
1334
+ );
1335
+ }
1336
+ console.log();
1337
+ }
1338
+ console.log(`
1339
+ ${successCount} of ${updatable.length} server(s) updated.
1340
+ `);
1341
+ }
1342
+
1343
+ // src/commands/config.ts
1344
+ init_cjs_shims();
1345
+ var import_citty2 = require("citty");
1346
+ var import_picocolors2 = __toESM(require("picocolors"), 1);
1347
+ var p2 = __toESM(require("@clack/prompts"), 1);
1348
+
1349
+ // src/core/config-service.ts
1350
+ init_cjs_shims();
1351
+ var import_node_fs4 = __toESM(require("fs"), 1);
1352
+ var import_node_path5 = __toESM(require("path"), 1);
1353
+ init_paths();
1354
+ var VALID_KEYS = /* @__PURE__ */ new Set([
1355
+ "defaultClient",
1356
+ "updateCheckInterval",
1357
+ "preferredRegistry",
1358
+ "vaultTimeout"
1359
+ ]);
1360
+ function readConfig(configPath = getConfigPath()) {
1361
+ try {
1362
+ const raw = import_node_fs4.default.readFileSync(configPath, "utf-8");
1363
+ const parsed = JSON.parse(raw);
1364
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1365
+ return {};
1366
+ }
1367
+ return parsed;
1368
+ } catch {
1369
+ return {};
1370
+ }
1371
+ }
1372
+ function writeConfig(data, configPath = getConfigPath()) {
1373
+ const dir = import_node_path5.default.dirname(configPath);
1374
+ import_node_fs4.default.mkdirSync(dir, { recursive: true });
1375
+ const tmp = `${configPath}.tmp`;
1376
+ import_node_fs4.default.writeFileSync(tmp, JSON.stringify(data, null, 2), { encoding: "utf-8" });
1377
+ import_node_fs4.default.renameSync(tmp, configPath);
1378
+ }
1379
+ function getConfigValue(key, configPath = getConfigPath()) {
1380
+ const data = readConfig(configPath);
1381
+ if (!VALID_KEYS.has(key)) return void 0;
1382
+ return data[key];
1383
+ }
1384
+ function setConfigValue(key, value, configPath = getConfigPath()) {
1385
+ const data = readConfig(configPath);
1386
+ if (!VALID_KEYS.has(key)) {
1387
+ throw new Error(`Unknown config key: "${key}". Valid keys: ${[...VALID_KEYS].join(", ")}`);
1388
+ }
1389
+ data[key] = value;
1390
+ writeConfig(data, configPath);
1391
+ }
1392
+
1393
+ // src/commands/config.ts
1394
+ function coerceValue(raw) {
1395
+ if (raw === "true") return true;
1396
+ if (raw === "false") return false;
1397
+ const num = Number(raw);
1398
+ if (!Number.isNaN(num) && raw.trim() !== "") return num;
1399
+ return raw;
1400
+ }
1401
+ var setCommand = (0, import_citty2.defineCommand)({
1402
+ meta: { name: "set", description: "Set a config value" },
1403
+ args: {
1404
+ key: {
1405
+ type: "positional",
1406
+ description: "Config key (e.g. defaultClient)",
1407
+ required: true
1408
+ },
1409
+ value: {
1410
+ type: "positional",
1411
+ description: "Value to set",
1412
+ required: true
1413
+ }
1414
+ },
1415
+ run({ args }) {
1416
+ try {
1417
+ const coerced = coerceValue(args.value);
1418
+ setConfigValue(args.key, coerced);
1419
+ console.log(
1420
+ `${import_picocolors2.default.green("\u2713")} Set ${import_picocolors2.default.bold(args.key)} = ${import_picocolors2.default.cyan(String(coerced))}`
1421
+ );
1422
+ } catch (err) {
1423
+ console.error(`${import_picocolors2.default.red("\u2717")} ${String(err)}`);
1424
+ process.exit(1);
1425
+ }
1426
+ }
1427
+ });
1428
+ var getCommand = (0, import_citty2.defineCommand)({
1429
+ meta: { name: "get", description: "Get a config value" },
1430
+ args: {
1431
+ key: {
1432
+ type: "positional",
1433
+ description: "Config key to read",
1434
+ required: true
1435
+ }
1436
+ },
1437
+ run({ args }) {
1438
+ const val = getConfigValue(args.key);
1439
+ if (val === void 0) {
1440
+ console.log(import_picocolors2.default.dim(`${args.key}: (not set)`));
1441
+ } else {
1442
+ console.log(`${import_picocolors2.default.bold(args.key)}: ${import_picocolors2.default.cyan(String(val))}`);
1443
+ }
1444
+ }
1445
+ });
1446
+ var listCommand = (0, import_citty2.defineCommand)({
1447
+ meta: { name: "list", description: "List all config values" },
1448
+ run() {
1449
+ const data = readConfig();
1450
+ const entries = Object.entries(data);
1451
+ if (entries.length === 0) {
1452
+ console.log(import_picocolors2.default.dim("No config values set. Use `mcpman config set <key> <value>`."));
1453
+ return;
1454
+ }
1455
+ console.log("");
1456
+ console.log(import_picocolors2.default.bold("mcpman config:"));
1457
+ console.log("");
1458
+ for (const [key, val] of entries) {
1459
+ console.log(` ${import_picocolors2.default.green("\u25CF")} ${import_picocolors2.default.bold(key)} ${import_picocolors2.default.cyan(String(val))}`);
1460
+ }
1461
+ console.log("");
1462
+ console.log(import_picocolors2.default.dim(` ${entries.length} key${entries.length !== 1 ? "s" : ""} configured`));
1463
+ }
1464
+ });
1465
+ var resetCommand = (0, import_citty2.defineCommand)({
1466
+ meta: { name: "reset", description: "Reset config to defaults (removes config file)" },
1467
+ async run() {
1468
+ const confirmed = await p2.confirm({
1469
+ message: "Reset all config values to defaults?",
1470
+ initialValue: false
1471
+ });
1472
+ if (p2.isCancel(confirmed) || !confirmed) {
1473
+ p2.cancel("Cancelled.");
1474
+ return;
1475
+ }
1476
+ writeConfig({});
1477
+ console.log(`${import_picocolors2.default.green("\u2713")} Config reset to defaults.`);
1478
+ }
1479
+ });
1480
+ var config_default = (0, import_citty2.defineCommand)({
1481
+ meta: {
1482
+ name: "config",
1483
+ description: "Manage mcpman CLI configuration"
1484
+ },
1485
+ subCommands: {
1486
+ set: setCommand,
1487
+ get: getCommand,
1488
+ list: listCommand,
1489
+ reset: resetCommand
1490
+ }
1491
+ });
1492
+
1493
+ // src/commands/doctor.ts
1494
+ init_cjs_shims();
1495
+ var import_citty3 = require("citty");
1496
+ var import_picocolors3 = __toESM(require("picocolors"), 1);
1497
+
1498
+ // src/core/server-inventory.ts
1499
+ init_cjs_shims();
1500
+ init_client_detector();
1501
+ async function getInstalledServers(clientFilter) {
1502
+ const clients = await getInstalledClients();
1503
+ const filtered = clientFilter ? clients.filter((c) => c.type === clientFilter) : clients;
1504
+ const serverMap = /* @__PURE__ */ new Map();
1505
+ for (const client of filtered) {
1506
+ let config;
1507
+ try {
1508
+ config = await client.readConfig();
1509
+ } catch {
1510
+ continue;
1511
+ }
1512
+ for (const [name, entry] of Object.entries(config.servers)) {
1513
+ const existing = serverMap.get(name);
944
1514
  if (existing) {
945
1515
  if (!existing.clients.includes(client.type)) {
946
1516
  existing.clients.push(client.type);
@@ -1194,12 +1764,12 @@ async function quickHealthProbe(config, timeoutMs = 3e3) {
1194
1764
 
1195
1765
  // src/commands/doctor.ts
1196
1766
  var CHECK_ICON = {
1197
- pass: import_picocolors2.default.green("\u2713"),
1198
- fail: import_picocolors2.default.red("\u2717"),
1199
- skip: import_picocolors2.default.dim("-"),
1200
- warn: import_picocolors2.default.yellow("\u26A0")
1767
+ pass: import_picocolors3.default.green("\u2713"),
1768
+ fail: import_picocolors3.default.red("\u2717"),
1769
+ skip: import_picocolors3.default.dim("-"),
1770
+ warn: import_picocolors3.default.yellow("\u26A0")
1201
1771
  };
1202
- var doctor_default = (0, import_citty2.defineCommand)({
1772
+ var doctor_default = (0, import_citty3.defineCommand)({
1203
1773
  meta: {
1204
1774
  name: "doctor",
1205
1775
  description: "Check MCP server health and configuration"
@@ -1212,10 +1782,10 @@ var doctor_default = (0, import_citty2.defineCommand)({
1212
1782
  }
1213
1783
  },
1214
1784
  async run({ args }) {
1215
- console.log(import_picocolors2.default.bold("\n mcpman doctor\n"));
1785
+ console.log(import_picocolors3.default.bold("\n mcpman doctor\n"));
1216
1786
  const servers = await getInstalledServers();
1217
1787
  if (servers.length === 0) {
1218
- console.log(import_picocolors2.default.dim(" No MCP servers installed. Run mcpman install <server> to get started."));
1788
+ console.log(import_picocolors3.default.dim(" No MCP servers installed. Run mcpman install <server> to get started."));
1219
1789
  return;
1220
1790
  }
1221
1791
  const tasks = servers.map((s) => () => checkServerHealth(s.name, s.config));
@@ -1227,14 +1797,14 @@ var doctor_default = (0, import_citty2.defineCommand)({
1227
1797
  if (result.status === "healthy") passed++;
1228
1798
  else failed++;
1229
1799
  }
1230
- console.log(import_picocolors2.default.dim(" " + "\u2500".repeat(50)));
1800
+ console.log(import_picocolors3.default.dim(" " + "\u2500".repeat(50)));
1231
1801
  const parts = [];
1232
- if (passed > 0) parts.push(import_picocolors2.default.green(`${passed} healthy`));
1233
- if (failed > 0) parts.push(import_picocolors2.default.red(`${failed} unhealthy`));
1802
+ if (passed > 0) parts.push(import_picocolors3.default.green(`${passed} healthy`));
1803
+ if (failed > 0) parts.push(import_picocolors3.default.red(`${failed} unhealthy`));
1234
1804
  console.log(` Summary: ${parts.join(", ")}`);
1235
1805
  if (failed > 0) {
1236
1806
  if (!args.fix) {
1237
- console.log(import_picocolors2.default.dim(` Run ${import_picocolors2.default.cyan("mcpman doctor --fix")} for fix suggestions.
1807
+ console.log(import_picocolors3.default.dim(` Run ${import_picocolors3.default.cyan("mcpman doctor --fix")} for fix suggestions.
1238
1808
  `));
1239
1809
  }
1240
1810
  process.exit(1);
@@ -1243,13 +1813,13 @@ var doctor_default = (0, import_citty2.defineCommand)({
1243
1813
  }
1244
1814
  });
1245
1815
  function printServerResult(result, showFix) {
1246
- const icon = result.status === "healthy" ? import_picocolors2.default.green("\u25CF") : import_picocolors2.default.red("\u25CF");
1247
- console.log(` ${icon} ${import_picocolors2.default.bold(result.serverName)}`);
1816
+ const icon = result.status === "healthy" ? import_picocolors3.default.green("\u25CF") : import_picocolors3.default.red("\u25CF");
1817
+ console.log(` ${icon} ${import_picocolors3.default.bold(result.serverName)}`);
1248
1818
  for (const check of result.checks) {
1249
1819
  const checkIcon = check.skipped ? CHECK_ICON.skip : check.passed ? CHECK_ICON.pass : CHECK_ICON.fail;
1250
1820
  console.log(` ${checkIcon} ${check.name}: ${check.message}`);
1251
1821
  if (showFix && !check.passed && !check.skipped && check.fix) {
1252
- console.log(` ${import_picocolors2.default.yellow("\u2192")} Fix: ${import_picocolors2.default.cyan(check.fix)}`);
1822
+ console.log(` ${import_picocolors3.default.yellow("\u2192")} Fix: ${import_picocolors3.default.cyan(check.fix)}`);
1253
1823
  }
1254
1824
  }
1255
1825
  console.log();
@@ -1258,11 +1828,11 @@ async function runParallel(tasks, concurrency) {
1258
1828
  const results = [];
1259
1829
  const executing = /* @__PURE__ */ new Set();
1260
1830
  for (const task of tasks) {
1261
- const p9 = task().then((r) => {
1831
+ const p12 = task().then((r) => {
1262
1832
  results.push(r);
1263
- executing.delete(p9);
1833
+ executing.delete(p12);
1264
1834
  });
1265
- executing.add(p9);
1835
+ executing.add(p12);
1266
1836
  if (executing.size >= concurrency) {
1267
1837
  await Promise.race(executing);
1268
1838
  }
@@ -1271,123 +1841,177 @@ async function runParallel(tasks, concurrency) {
1271
1841
  return results;
1272
1842
  }
1273
1843
 
1274
- // src/commands/init.ts
1844
+ // src/commands/info.ts
1275
1845
  init_cjs_shims();
1276
- var import_citty3 = require("citty");
1277
- var p = __toESM(require("@clack/prompts"), 1);
1278
- var import_node_path5 = __toESM(require("path"), 1);
1846
+ var import_citty4 = require("citty");
1847
+ var import_picocolors4 = __toESM(require("picocolors"), 1);
1848
+ var import_nanospinner2 = require("nanospinner");
1279
1849
 
1280
- // src/core/registry.ts
1850
+ // src/core/package-info.ts
1281
1851
  init_cjs_shims();
1282
- var import_node_crypto = require("crypto");
1283
- function computeIntegrity(resolvedUrl) {
1284
- const hash = (0, import_node_crypto.createHash)("sha512").update(resolvedUrl).digest("base64");
1285
- return `sha512-${hash}`;
1286
- }
1287
- async function resolveFromSmithery(name) {
1288
- const url = `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`;
1289
- let data;
1290
- try {
1291
- const res = await fetch(url, {
1292
- headers: { Accept: "application/json" },
1293
- signal: AbortSignal.timeout(8e3)
1294
- });
1295
- if (res.status === 404) {
1296
- throw new Error(`Server '${name}' not found on Smithery registry`);
1297
- }
1298
- if (!res.ok) {
1299
- throw new Error(`Smithery API error: ${res.status}`);
1852
+ init_trust_scorer();
1853
+ async function buildInfo(name, entry, source = "npm") {
1854
+ const resolvedSource = entry?.source ?? source;
1855
+ let weeklyDownloads = 0;
1856
+ let maintainerCount = 0;
1857
+ let packageAge = 0;
1858
+ let lastPublish = "";
1859
+ let deprecated = false;
1860
+ let trustScore = null;
1861
+ let riskLevel = "UNKNOWN";
1862
+ if (resolvedSource === "npm") {
1863
+ const metadata = await fetchNpmMetadata(name);
1864
+ if (!metadata && !entry) {
1865
+ return null;
1866
+ }
1867
+ if (metadata) {
1868
+ weeklyDownloads = metadata.weeklyDownloads;
1869
+ maintainerCount = metadata.maintainerCount;
1870
+ packageAge = metadata.packageAge;
1871
+ lastPublish = metadata.lastPublish;
1872
+ deprecated = metadata.deprecated;
1873
+ const scored = computeTrustScore(metadata, []);
1874
+ trustScore = scored.score;
1875
+ riskLevel = scored.riskLevel;
1300
1876
  }
1301
- data = await res.json();
1302
- } catch (err) {
1303
- if (err instanceof Error && err.message.includes("not found")) throw err;
1304
- throw new Error(
1305
- `Cannot reach Smithery registry: ${err instanceof Error ? err.message : String(err)}`
1306
- );
1307
1877
  }
1308
- const version = typeof data.version === "string" ? data.version : "latest";
1309
- const command = typeof data.command === "string" ? data.command : "npx";
1310
- const args = Array.isArray(data.args) ? data.args : ["-y", `${name}@${version}`];
1311
- const envVars = Array.isArray(data.envVars) ? data.envVars : [];
1312
- const resolved = typeof data.resolved === "string" ? data.resolved : `smithery:${name}@${version}`;
1313
1878
  return {
1314
1879
  name,
1315
- version,
1316
- description: typeof data.description === "string" ? data.description : "",
1317
- runtime: "node",
1318
- command,
1319
- args,
1320
- envVars,
1321
- resolved
1880
+ version: entry?.version ?? "unknown",
1881
+ description: "",
1882
+ source: resolvedSource,
1883
+ runtime: entry?.runtime ?? "node",
1884
+ envVars: entry?.envVars ?? [],
1885
+ weeklyDownloads,
1886
+ maintainerCount,
1887
+ packageAge,
1888
+ lastPublish,
1889
+ deprecated,
1890
+ trustScore,
1891
+ riskLevel,
1892
+ installedClients: entry?.clients ?? [],
1893
+ isInstalled: entry !== null
1322
1894
  };
1323
1895
  }
1324
- async function resolveFromNpm(packageName) {
1325
- const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
1326
- let data;
1327
- try {
1328
- const res = await fetch(url, {
1329
- headers: { Accept: "application/json" },
1330
- signal: AbortSignal.timeout(8e3)
1331
- });
1332
- if (res.status === 404) {
1333
- throw new Error(`Package '${packageName}' not found on npm`);
1334
- }
1335
- if (!res.ok) {
1336
- throw new Error(`npm registry error: ${res.status}`);
1337
- }
1338
- data = await res.json();
1339
- } catch (err) {
1340
- if (err instanceof Error && err.message.includes("not found")) throw err;
1341
- throw new Error(
1342
- `Cannot reach npm registry: ${err instanceof Error ? err.message : String(err)}`
1343
- );
1344
- }
1345
- const version = typeof data.version === "string" ? data.version : "latest";
1346
- const resolved = `https://registry.npmjs.org/${packageName}/-/${packageName.replace(/^@[^/]+\//, "")}-${version}.tgz`;
1347
- const mcpField = data.mcp && typeof data.mcp === "object" ? data.mcp : null;
1348
- const envVars = mcpField?.envVars ? mcpField.envVars : [];
1349
- return {
1350
- name: packageName,
1351
- version,
1352
- description: typeof data.description === "string" ? data.description : "",
1353
- runtime: "node",
1354
- command: "npx",
1355
- args: ["-y", `${packageName}@${version}`],
1356
- envVars,
1357
- resolved
1358
- };
1896
+ async function getPackageInfo(serverName) {
1897
+ const lockfile = readLockfile();
1898
+ const entry = lockfile.servers[serverName] ?? null;
1899
+ return buildInfo(serverName, entry);
1359
1900
  }
1360
- async function resolveFromGitHub(githubUrl) {
1361
- const match = githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
1362
- if (!match) {
1363
- throw new Error(`Invalid GitHub URL: ${githubUrl}`);
1901
+
1902
+ // src/commands/info.ts
1903
+ function colorRisk2(score, riskLevel) {
1904
+ const label = score !== null ? `${score}/100 (${riskLevel})` : riskLevel;
1905
+ if (riskLevel === "LOW") return import_picocolors4.default.green(label);
1906
+ if (riskLevel === "MEDIUM") return import_picocolors4.default.yellow(label);
1907
+ if (riskLevel === "HIGH") return import_picocolors4.default.red(label);
1908
+ if (riskLevel === "CRITICAL") return import_picocolors4.default.bold(import_picocolors4.default.red(label));
1909
+ return import_picocolors4.default.dim(label);
1910
+ }
1911
+ function formatDaysAgo(isoDate) {
1912
+ if (!isoDate) return "unknown";
1913
+ const days = Math.floor((Date.now() - new Date(isoDate).getTime()) / 864e5);
1914
+ if (days === 0) return "today";
1915
+ if (days === 1) return "1 day ago";
1916
+ return `${days} days ago`;
1917
+ }
1918
+ function printInfo(info2) {
1919
+ const installedBadge = info2.isInstalled ? import_picocolors4.default.green(" [installed]") : import_picocolors4.default.dim(" [not installed]");
1920
+ console.log();
1921
+ console.log(import_picocolors4.default.bold(` ${info2.name}@${info2.version}`) + installedBadge);
1922
+ console.log(import_picocolors4.default.dim(" " + "\u2500".repeat(60)));
1923
+ console.log(` ${import_picocolors4.default.dim("Source:")} ${info2.source}`);
1924
+ console.log(` ${import_picocolors4.default.dim("Runtime:")} ${info2.runtime}`);
1925
+ if (info2.description) {
1926
+ console.log(` ${import_picocolors4.default.dim("Description:")} ${info2.description}`);
1927
+ }
1928
+ if (info2.deprecated) {
1929
+ console.log(` ${import_picocolors4.default.red("[DEPRECATED]")} This package is deprecated`);
1930
+ }
1931
+ console.log();
1932
+ console.log(` ${import_picocolors4.default.bold("Trust & Security")}`);
1933
+ console.log(` ${import_picocolors4.default.dim("Trust score:")} ${colorRisk2(info2.trustScore, info2.riskLevel)}`);
1934
+ if (info2.source === "npm") {
1935
+ console.log(
1936
+ ` ${import_picocolors4.default.dim("Downloads:")} ${info2.weeklyDownloads.toLocaleString()}/week ${import_picocolors4.default.dim("|")} ${import_picocolors4.default.dim("Age:")} ${info2.packageAge}d ${import_picocolors4.default.dim("|")} ${import_picocolors4.default.dim("Maintainers:")} ${info2.maintainerCount}`
1937
+ );
1938
+ if (info2.lastPublish) {
1939
+ console.log(` ${import_picocolors4.default.dim("Last publish:")} ${formatDaysAgo(info2.lastPublish)}`);
1940
+ }
1941
+ } else {
1942
+ console.log(import_picocolors4.default.dim(" (Trust data available for npm packages only)"));
1364
1943
  }
1365
- const [, owner, repo] = match;
1366
- const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/package.json`;
1367
- let pkgData = {};
1368
- try {
1369
- const res = await fetch(rawUrl, { signal: AbortSignal.timeout(8e3) });
1370
- if (res.ok) {
1371
- pkgData = await res.json();
1944
+ console.log();
1945
+ console.log(` ${import_picocolors4.default.bold("Environment Variables")}`);
1946
+ if (info2.envVars.length > 0) {
1947
+ for (const env of info2.envVars) {
1948
+ console.log(` ${import_picocolors4.default.cyan("\u2022")} ${env}`);
1372
1949
  }
1373
- } catch {
1950
+ } else {
1951
+ console.log(import_picocolors4.default.dim(" none required"));
1374
1952
  }
1375
- const version = typeof pkgData.version === "string" ? pkgData.version : "main";
1376
- const name = typeof pkgData.name === "string" ? pkgData.name : `${owner}/${repo}`;
1377
- return {
1378
- name,
1379
- version,
1380
- description: typeof pkgData.description === "string" ? pkgData.description : "",
1381
- runtime: "node",
1382
- command: "npx",
1383
- args: ["-y", githubUrl],
1384
- envVars: [],
1385
- resolved: githubUrl
1386
- };
1953
+ console.log();
1954
+ console.log(` ${import_picocolors4.default.bold("Installed Clients")}`);
1955
+ if (info2.installedClients.length > 0) {
1956
+ for (const client of info2.installedClients) {
1957
+ console.log(` ${import_picocolors4.default.green("\u2713")} ${client}`);
1958
+ }
1959
+ } else {
1960
+ console.log(import_picocolors4.default.dim(" Not installed in any client"));
1961
+ }
1962
+ console.log();
1963
+ console.log(import_picocolors4.default.dim(" " + "\u2500".repeat(60)));
1964
+ console.log();
1387
1965
  }
1966
+ var info_default = (0, import_citty4.defineCommand)({
1967
+ meta: {
1968
+ name: "info",
1969
+ description: "Show detailed metadata for an MCP server (installed or from registry)"
1970
+ },
1971
+ args: {
1972
+ server: {
1973
+ type: "positional",
1974
+ description: "Server name (e.g. @modelcontextprotocol/server-filesystem)",
1975
+ required: true
1976
+ },
1977
+ json: {
1978
+ type: "boolean",
1979
+ description: "Output results as JSON",
1980
+ default: false
1981
+ }
1982
+ },
1983
+ async run({ args }) {
1984
+ const spinner5 = (0, import_nanospinner2.createSpinner)(`Fetching info for ${args.server}...`).start();
1985
+ let info2;
1986
+ try {
1987
+ info2 = await getPackageInfo(args.server);
1988
+ } catch (err) {
1989
+ spinner5.error({ text: "Failed to fetch package info" });
1990
+ console.error(import_picocolors4.default.red(String(err)));
1991
+ process.exit(1);
1992
+ }
1993
+ if (!info2) {
1994
+ spinner5.error({ text: `Package not found: ${args.server}` });
1995
+ console.log(import_picocolors4.default.dim(`
1996
+ "${args.server}" was not found in the npm registry or your lockfile.
1997
+ `));
1998
+ process.exit(1);
1999
+ }
2000
+ spinner5.success({ text: `Found ${args.server}` });
2001
+ if (args.json) {
2002
+ console.log(JSON.stringify(info2, null, 2));
2003
+ return;
2004
+ }
2005
+ printInfo(info2);
2006
+ }
2007
+ });
1388
2008
 
1389
2009
  // src/commands/init.ts
1390
- var init_default = (0, import_citty3.defineCommand)({
2010
+ init_cjs_shims();
2011
+ var import_citty5 = require("citty");
2012
+ var p3 = __toESM(require("@clack/prompts"), 1);
2013
+ var import_node_path6 = __toESM(require("path"), 1);
2014
+ var init_default = (0, import_citty5.defineCommand)({
1391
2015
  meta: {
1392
2016
  name: "init",
1393
2017
  description: "Initialize mcpman.lock in the current project"
@@ -1402,17 +2026,17 @@ var init_default = (0, import_citty3.defineCommand)({
1402
2026
  },
1403
2027
  async run({ args }) {
1404
2028
  const nonInteractive = args.yes || !process.stdout.isTTY;
1405
- p.intro("mcpman init");
1406
- const targetPath = import_node_path5.default.join(process.cwd(), LOCKFILE_NAME);
2029
+ p3.intro("mcpman init");
2030
+ const targetPath = import_node_path6.default.join(process.cwd(), LOCKFILE_NAME);
1407
2031
  const existing = findLockfile();
1408
2032
  if (existing) {
1409
2033
  if (nonInteractive) {
1410
- p.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
2034
+ p3.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
1411
2035
  } else {
1412
- p.log.warn(`Lockfile already exists: ${existing}`);
1413
- const overwrite = await p.confirm({ message: "Overwrite?" });
1414
- if (p.isCancel(overwrite) || !overwrite) {
1415
- p.outro("Cancelled.");
2036
+ p3.log.warn(`Lockfile already exists: ${existing}`);
2037
+ const overwrite = await p3.confirm({ message: "Overwrite?" });
2038
+ if (p3.isCancel(overwrite) || !overwrite) {
2039
+ p3.outro("Cancelled.");
1416
2040
  return;
1417
2041
  }
1418
2042
  }
@@ -1422,7 +2046,7 @@ var init_default = (0, import_citty3.defineCommand)({
1422
2046
  const mod = await Promise.resolve().then(() => (init_client_detector(), client_detector_exports));
1423
2047
  clients = await mod.getInstalledClients();
1424
2048
  } catch {
1425
- p.log.warn("Could not detect AI clients \u2014 creating empty lockfile.");
2049
+ p3.log.warn("Could not detect AI clients \u2014 creating empty lockfile.");
1426
2050
  }
1427
2051
  const clientServers = [];
1428
2052
  for (const client of clients) {
@@ -1436,26 +2060,26 @@ var init_default = (0, import_citty3.defineCommand)({
1436
2060
  }
1437
2061
  createEmptyLockfile(targetPath);
1438
2062
  if (clientServers.length === 0) {
1439
- p.log.info("No existing servers found in any client config.");
1440
- p.outro(`Created ${LOCKFILE_NAME} \u2014 add it to version control!`);
2063
+ p3.log.info("No existing servers found in any client config.");
2064
+ p3.outro(`Created ${LOCKFILE_NAME} \u2014 add it to version control!`);
1441
2065
  return;
1442
2066
  }
1443
2067
  let selected;
1444
2068
  if (nonInteractive) {
1445
2069
  selected = clientServers.map((cs) => cs.client.type);
1446
- p.log.info(`Non-interactive mode: importing all ${clientServers.length} client(s).`);
2070
+ p3.log.info(`Non-interactive mode: importing all ${clientServers.length} client(s).`);
1447
2071
  } else {
1448
2072
  const options = clientServers.map((cs) => ({
1449
2073
  value: cs.client.type,
1450
2074
  label: `${cs.client.displayName} (${Object.keys(cs.servers).length} servers)`
1451
2075
  }));
1452
- const toImport = await p.multiselect({
2076
+ const toImport = await p3.multiselect({
1453
2077
  message: "Import existing servers into lockfile?",
1454
2078
  options,
1455
2079
  required: false
1456
2080
  });
1457
- if (p.isCancel(toImport)) {
1458
- p.outro(`Created empty ${LOCKFILE_NAME}`);
2081
+ if (p3.isCancel(toImport)) {
2082
+ p3.outro(`Created empty ${LOCKFILE_NAME}`);
1459
2083
  return;
1460
2084
  }
1461
2085
  selected = toImport;
@@ -1481,7 +2105,7 @@ var init_default = (0, import_citty3.defineCommand)({
1481
2105
  importCount++;
1482
2106
  }
1483
2107
  }
1484
- p.outro(
2108
+ p3.outro(
1485
2109
  `Created ${LOCKFILE_NAME} with ${importCount} server(s) \u2014 commit to version control!`
1486
2110
  );
1487
2111
  }
@@ -1489,49 +2113,48 @@ var init_default = (0, import_citty3.defineCommand)({
1489
2113
 
1490
2114
  // src/commands/install.ts
1491
2115
  init_cjs_shims();
1492
- var import_citty4 = require("citty");
2116
+ var import_citty6 = require("citty");
1493
2117
 
1494
2118
  // src/core/installer.ts
1495
2119
  init_cjs_shims();
1496
- var p2 = __toESM(require("@clack/prompts"), 1);
2120
+ var p6 = __toESM(require("@clack/prompts"), 1);
1497
2121
 
1498
- // src/core/server-resolver.ts
2122
+ // src/core/installer-vault-helpers.ts
1499
2123
  init_cjs_shims();
1500
- function detectSource(input) {
1501
- if (input.startsWith("smithery:")) {
1502
- return { type: "smithery", input: input.slice(9) };
1503
- }
1504
- if (input.startsWith("https://github.com/") || input.startsWith("github.com/")) {
1505
- return { type: "github", input };
1506
- }
1507
- return { type: "npm", input };
1508
- }
1509
- function parseEnvFlags(envFlags) {
1510
- if (!envFlags) return {};
1511
- const flags = Array.isArray(envFlags) ? envFlags : [envFlags];
1512
- const result = {};
1513
- for (const flag of flags) {
1514
- const idx = flag.indexOf("=");
1515
- if (idx > 0) {
1516
- result[flag.slice(0, idx)] = flag.slice(idx + 1);
2124
+ var p5 = __toESM(require("@clack/prompts"), 1);
2125
+ init_vault_service();
2126
+ async function tryLoadVaultSecrets(serverName) {
2127
+ try {
2128
+ const entries = listSecrets(serverName);
2129
+ if (entries.length === 0 || entries[0].keys.length === 0) {
2130
+ return {};
1517
2131
  }
2132
+ const password2 = await getMasterPassword();
2133
+ return getSecretsForServer(serverName, password2);
2134
+ } catch {
2135
+ return {};
1518
2136
  }
1519
- return result;
1520
2137
  }
1521
- async function resolveServer(input) {
1522
- const source = detectSource(input);
1523
- switch (source.type) {
1524
- case "smithery":
1525
- return resolveFromSmithery(source.input);
1526
- case "github":
1527
- return resolveFromGitHub(source.input);
1528
- case "npm":
1529
- return resolveFromNpm(source.input);
2138
+ async function offerVaultSave(serverName, newVars, yes) {
2139
+ if (Object.keys(newVars).length === 0) return;
2140
+ if (yes) return;
2141
+ try {
2142
+ const save = await p5.confirm({
2143
+ message: `Save ${Object.keys(newVars).length} env var(s) to encrypted vault for future installs?`
2144
+ });
2145
+ if (p5.isCancel(save) || !save) return;
2146
+ const password2 = await getMasterPassword();
2147
+ for (const [key, value] of Object.entries(newVars)) {
2148
+ setSecret(serverName, key, value, password2);
2149
+ }
2150
+ p5.log.success(`Credentials saved to vault for '${serverName}'`);
2151
+ } catch (err) {
2152
+ p5.log.warn(`Could not save to vault: ${err instanceof Error ? err.message : String(err)}`);
1530
2153
  }
1531
2154
  }
1532
2155
 
1533
2156
  // src/core/installer.ts
1534
- async function loadClients() {
2157
+ async function loadClients2() {
1535
2158
  try {
1536
2159
  const mod = await Promise.resolve().then(() => (init_client_detector(), client_detector_exports));
1537
2160
  return mod.getInstalledClients();
@@ -1540,65 +2163,68 @@ async function loadClients() {
1540
2163
  }
1541
2164
  }
1542
2165
  async function installServer(input, options = {}) {
1543
- p2.intro("mcpman install");
1544
- const spinner5 = p2.spinner();
2166
+ p6.intro("mcpman install");
2167
+ const spinner5 = p6.spinner();
1545
2168
  spinner5.start("Resolving server...");
1546
2169
  let metadata;
1547
2170
  try {
1548
2171
  metadata = await resolveServer(input);
1549
2172
  } catch (err) {
1550
2173
  spinner5.stop("Resolution failed");
1551
- p2.log.error(err instanceof Error ? err.message : String(err));
2174
+ p6.log.error(err instanceof Error ? err.message : String(err));
1552
2175
  process.exit(1);
1553
2176
  }
1554
2177
  spinner5.stop(`Found: ${metadata.name}@${metadata.version}`);
1555
- const clients = await loadClients();
2178
+ const clients = await loadClients2();
1556
2179
  if (clients.length === 0) {
1557
- p2.log.warn("No supported AI clients detected on this machine.");
1558
- p2.log.info("Supported: Claude Desktop, Cursor, VS Code, Windsurf");
2180
+ p6.log.warn("No supported AI clients detected on this machine.");
2181
+ p6.log.info("Supported: Claude Desktop, Cursor, VS Code, Windsurf");
1559
2182
  process.exit(1);
1560
2183
  }
1561
2184
  let selectedClients;
1562
2185
  if (options.client) {
1563
2186
  const found = clients.find((c) => c.type === options.client || c.displayName.toLowerCase() === options.client?.toLowerCase());
1564
2187
  if (!found) {
1565
- p2.log.error(`Client '${options.client}' not found or not installed.`);
1566
- p2.log.info(`Available: ${clients.map((c) => c.type).join(", ")}`);
2188
+ p6.log.error(`Client '${options.client}' not found or not installed.`);
2189
+ p6.log.info(`Available: ${clients.map((c) => c.type).join(", ")}`);
1567
2190
  process.exit(1);
1568
2191
  }
1569
2192
  selectedClients = [found];
1570
2193
  } else if (options.yes || clients.length === 1) {
1571
2194
  selectedClients = clients;
1572
2195
  } else {
1573
- const chosen = await p2.multiselect({
2196
+ const chosen = await p6.multiselect({
1574
2197
  message: "Install to which client(s)?",
1575
2198
  options: clients.map((c) => ({ value: c.type, label: c.displayName })),
1576
2199
  required: true
1577
2200
  });
1578
- if (p2.isCancel(chosen)) {
1579
- p2.outro("Cancelled.");
2201
+ if (p6.isCancel(chosen)) {
2202
+ p6.outro("Cancelled.");
1580
2203
  process.exit(0);
1581
2204
  }
1582
2205
  selectedClients = clients.filter((c) => chosen.includes(c.type));
1583
2206
  }
1584
2207
  const providedEnv = parseEnvFlags(options.env);
1585
- const collectedEnv = { ...providedEnv };
2208
+ const vaultEnv = await tryLoadVaultSecrets(metadata.name);
2209
+ const collectedEnv = { ...vaultEnv, ...providedEnv };
2210
+ const newlyEnteredVars = {};
1586
2211
  const requiredVars = metadata.envVars.filter((e) => e.required && !(e.name in collectedEnv));
1587
2212
  for (const envVar of requiredVars) {
1588
2213
  if (options.yes && envVar.default) {
1589
2214
  collectedEnv[envVar.name] = envVar.default;
1590
2215
  continue;
1591
2216
  }
1592
- const val = await p2.text({
2217
+ const val = await p6.text({
1593
2218
  message: `${envVar.name}${envVar.description ? ` \u2014 ${envVar.description}` : ""}`,
1594
2219
  placeholder: envVar.default ?? "",
1595
2220
  validate: (v) => envVar.required && !v ? "Required" : void 0
1596
2221
  });
1597
- if (p2.isCancel(val)) {
1598
- p2.outro("Cancelled.");
2222
+ if (p6.isCancel(val)) {
2223
+ p6.outro("Cancelled.");
1599
2224
  process.exit(0);
1600
2225
  }
1601
2226
  collectedEnv[envVar.name] = val;
2227
+ newlyEnteredVars[envVar.name] = val;
1602
2228
  }
1603
2229
  const entry = {
1604
2230
  command: metadata.command,
@@ -1613,7 +2239,7 @@ async function installServer(input, options = {}) {
1613
2239
  clientTypes.push(client.type);
1614
2240
  } catch (err) {
1615
2241
  spinner5.stop("Partial failure");
1616
- p2.log.warn(`Failed to write to ${client.displayName}: ${err instanceof Error ? err.message : String(err)}`);
2242
+ p6.log.warn(`Failed to write to ${client.displayName}: ${err instanceof Error ? err.message : String(err)}`);
1617
2243
  }
1618
2244
  }
1619
2245
  spinner5.stop("Config written");
@@ -1632,13 +2258,14 @@ async function installServer(input, options = {}) {
1632
2258
  clients: clientTypes
1633
2259
  });
1634
2260
  const lockPath = findLockfile() ?? "mcpman.lock (global)";
1635
- p2.log.success(`Lockfile updated: ${lockPath}`);
1636
- p2.outro(`${metadata.name}@${metadata.version} installed to ${clientTypes.join(", ")}`);
2261
+ p6.log.success(`Lockfile updated: ${lockPath}`);
2262
+ await offerVaultSave(metadata.name, newlyEnteredVars, options.yes ?? false);
2263
+ p6.outro(`${metadata.name}@${metadata.version} installed to ${clientTypes.join(", ")}`);
1637
2264
  }
1638
2265
 
1639
2266
  // src/utils/logger.ts
1640
2267
  init_cjs_shims();
1641
- var import_picocolors3 = __toESM(require("picocolors"), 1);
2268
+ var import_picocolors5 = __toESM(require("picocolors"), 1);
1642
2269
  var noColor = process.env.NO_COLOR !== void 0 || process.argv.includes("--no-color");
1643
2270
  var isVerbose = process.argv.includes("--verbose");
1644
2271
  var isJson = process.argv.includes("--json");
@@ -1647,19 +2274,19 @@ function colorize(fn, text2) {
1647
2274
  }
1648
2275
  function info(message) {
1649
2276
  if (isJson) return;
1650
- console.log(`${colorize(import_picocolors3.default.cyan, "i")} ${message}`);
2277
+ console.log(`${colorize(import_picocolors5.default.cyan, "i")} ${message}`);
1651
2278
  }
1652
2279
  function error(message) {
1653
2280
  if (isJson) return;
1654
- console.error(`${colorize(import_picocolors3.default.red, "\u2717")} ${message}`);
2281
+ console.error(`${colorize(import_picocolors5.default.red, "\u2717")} ${message}`);
1655
2282
  }
1656
2283
  function json(data) {
1657
2284
  console.log(JSON.stringify(data, null, 2));
1658
2285
  }
1659
2286
 
1660
2287
  // src/commands/install.ts
1661
- var p3 = __toESM(require("@clack/prompts"), 1);
1662
- var install_default = (0, import_citty4.defineCommand)({
2288
+ var p7 = __toESM(require("@clack/prompts"), 1);
2289
+ var install_default = (0, import_citty6.defineCommand)({
1663
2290
  meta: {
1664
2291
  name: "install",
1665
2292
  description: "Install an MCP server into one or more AI clients"
@@ -1708,8 +2335,8 @@ async function restoreFromLockfile() {
1708
2335
  info("Lockfile is empty \u2014 nothing to restore.");
1709
2336
  return;
1710
2337
  }
1711
- p3.intro(`mcpman install (restore from ${lockPath})`);
1712
- p3.log.info(`Restoring ${entries.length} server(s)...`);
2338
+ p7.intro(`mcpman install (restore from ${lockPath})`);
2339
+ p7.log.info(`Restoring ${entries.length} server(s)...`);
1713
2340
  for (const [name, entry] of entries) {
1714
2341
  const input = entry.source === "smithery" ? `smithery:${name}` : entry.source === "github" ? entry.resolved : name;
1715
2342
  await installServer(input, {
@@ -1717,19 +2344,19 @@ async function restoreFromLockfile() {
1717
2344
  yes: true
1718
2345
  });
1719
2346
  }
1720
- p3.outro("Restore complete.");
2347
+ p7.outro("Restore complete.");
1721
2348
  }
1722
2349
 
1723
2350
  // src/commands/list.ts
1724
2351
  init_cjs_shims();
1725
- var import_citty5 = require("citty");
1726
- var import_picocolors4 = __toESM(require("picocolors"), 1);
2352
+ var import_citty7 = require("citty");
2353
+ var import_picocolors6 = __toESM(require("picocolors"), 1);
1727
2354
  var STATUS_ICON = {
1728
- healthy: import_picocolors4.default.green("\u25CF"),
1729
- unhealthy: import_picocolors4.default.red("\u25CF"),
1730
- unknown: import_picocolors4.default.dim("\u25CB")
2355
+ healthy: import_picocolors6.default.green("\u25CF"),
2356
+ unhealthy: import_picocolors6.default.red("\u25CF"),
2357
+ unknown: import_picocolors6.default.dim("\u25CB")
1731
2358
  };
1732
- var list_default = (0, import_citty5.defineCommand)({
2359
+ var list_default = (0, import_citty7.defineCommand)({
1733
2360
  meta: {
1734
2361
  name: "list",
1735
2362
  description: "List installed MCP servers"
@@ -1749,7 +2376,7 @@ var list_default = (0, import_citty5.defineCommand)({
1749
2376
  const servers = await getInstalledServers(args.client);
1750
2377
  if (servers.length === 0) {
1751
2378
  const filter = args.client ? ` for client "${args.client}"` : "";
1752
- console.log(import_picocolors4.default.dim(`No MCP servers installed${filter}. Run ${import_picocolors4.default.cyan("mcpman install <server>")} to get started.`));
2379
+ console.log(import_picocolors6.default.dim(`No MCP servers installed${filter}. Run ${import_picocolors6.default.cyan("mcpman install <server>")} to get started.`));
1753
2380
  return;
1754
2381
  }
1755
2382
  const withStatus = await Promise.all(
@@ -1773,8 +2400,8 @@ var list_default = (0, import_citty5.defineCommand)({
1773
2400
  const nameWidth = Math.max(4, ...withStatus.map((s) => s.name.length));
1774
2401
  const clientsWidth = Math.max(7, ...withStatus.map((s) => formatClients(s.clients).length));
1775
2402
  const header = ` ${pad("NAME", nameWidth)} ${pad("CLIENT(S)", clientsWidth)} ${pad("COMMAND", 20)} STATUS`;
1776
- console.log(import_picocolors4.default.dim(header));
1777
- console.log(import_picocolors4.default.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientsWidth)} ${"-".repeat(20)} ------`));
2403
+ console.log(import_picocolors6.default.dim(header));
2404
+ console.log(import_picocolors6.default.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientsWidth)} ${"-".repeat(20)} ------`));
1778
2405
  for (const s of withStatus) {
1779
2406
  const icon = STATUS_ICON[s.status];
1780
2407
  const clientsStr = formatClients(s.clients);
@@ -1782,7 +2409,7 @@ var list_default = (0, import_citty5.defineCommand)({
1782
2409
  console.log(` ${pad(s.name, nameWidth)} ${pad(clientsStr, clientsWidth)} ${pad(cmdStr, 20)} ${icon} ${s.status}`);
1783
2410
  }
1784
2411
  const clientSet = new Set(withStatus.flatMap((s) => s.clients));
1785
- console.log(import_picocolors4.default.dim(`
2412
+ console.log(import_picocolors6.default.dim(`
1786
2413
  ${withStatus.length} server${withStatus.length !== 1 ? "s" : ""} \xB7 ${clientSet.size} client${clientSet.size !== 1 ? "s" : ""}`));
1787
2414
  }
1788
2415
  });
@@ -1804,9 +2431,9 @@ function formatClients(clients) {
1804
2431
 
1805
2432
  // src/commands/remove.ts
1806
2433
  init_cjs_shims();
1807
- var import_citty6 = require("citty");
1808
- var p4 = __toESM(require("@clack/prompts"), 1);
1809
- var import_picocolors5 = __toESM(require("picocolors"), 1);
2434
+ var import_citty8 = require("citty");
2435
+ var p8 = __toESM(require("@clack/prompts"), 1);
2436
+ var import_picocolors7 = __toESM(require("picocolors"), 1);
1810
2437
  init_client_detector();
1811
2438
  var CLIENT_DISPLAY2 = {
1812
2439
  "claude-desktop": "Claude",
@@ -1817,113 +2444,360 @@ var CLIENT_DISPLAY2 = {
1817
2444
  function clientDisplayName(type) {
1818
2445
  return CLIENT_DISPLAY2[type] ?? type;
1819
2446
  }
1820
- var remove_default = (0, import_citty6.defineCommand)({
2447
+ var remove_default = (0, import_citty8.defineCommand)({
2448
+ meta: {
2449
+ name: "remove",
2450
+ description: "Remove an MCP server from one or more AI clients"
2451
+ },
2452
+ args: {
2453
+ server: {
2454
+ type: "positional",
2455
+ description: "Server name to remove",
2456
+ required: true
2457
+ },
2458
+ client: {
2459
+ type: "string",
2460
+ description: "Target client (claude, cursor, vscode, windsurf)"
2461
+ },
2462
+ all: {
2463
+ type: "boolean",
2464
+ description: "Remove from all clients",
2465
+ default: false
2466
+ },
2467
+ yes: {
2468
+ type: "boolean",
2469
+ description: "Skip confirmation prompt",
2470
+ default: false
2471
+ }
2472
+ },
2473
+ async run({ args }) {
2474
+ p8.intro(import_picocolors7.default.bold("mcpman remove"));
2475
+ const serverName = args.server;
2476
+ const servers = await getInstalledServers();
2477
+ const match = servers.find((s) => s.name === serverName);
2478
+ if (!match) {
2479
+ p8.log.warn(`Server "${serverName}" is not installed.`);
2480
+ const similar = servers.filter((s) => s.name.includes(serverName) || serverName.includes(s.name));
2481
+ if (similar.length > 0) {
2482
+ p8.log.info(`Did you mean: ${similar.map((s) => import_picocolors7.default.cyan(s.name)).join(", ")}?`);
2483
+ }
2484
+ p8.outro("Nothing to remove.");
2485
+ return;
2486
+ }
2487
+ let targetClients;
2488
+ if (args.all) {
2489
+ targetClients = match.clients;
2490
+ } else if (args.client) {
2491
+ if (!match.clients.includes(args.client)) {
2492
+ p8.log.warn(`Server "${serverName}" is not installed in client "${args.client}".`);
2493
+ p8.outro("Nothing to remove.");
2494
+ return;
2495
+ }
2496
+ targetClients = [args.client];
2497
+ } else if (match.clients.length === 1) {
2498
+ targetClients = match.clients;
2499
+ } else {
2500
+ const selected = await p8.multiselect({
2501
+ message: `Remove "${serverName}" from which clients?`,
2502
+ options: match.clients.map((c) => ({
2503
+ value: c,
2504
+ label: clientDisplayName(c)
2505
+ })),
2506
+ required: true
2507
+ });
2508
+ if (p8.isCancel(selected)) {
2509
+ p8.outro("Cancelled.");
2510
+ process.exit(0);
2511
+ }
2512
+ targetClients = selected;
2513
+ }
2514
+ if (!args.yes) {
2515
+ const clientNames = targetClients.map(clientDisplayName).join(", ");
2516
+ const confirmed = await p8.confirm({
2517
+ message: `Remove ${import_picocolors7.default.cyan(serverName)} from ${import_picocolors7.default.yellow(clientNames)}?`
2518
+ });
2519
+ if (p8.isCancel(confirmed) || !confirmed) {
2520
+ p8.outro("Cancelled.");
2521
+ return;
2522
+ }
2523
+ }
2524
+ const installedClients = await getInstalledClients();
2525
+ const errors = [];
2526
+ for (const clientType of targetClients) {
2527
+ const handler = installedClients.find((c) => c.type === clientType);
2528
+ if (!handler) {
2529
+ errors.push(`Client "${clientType}" not found`);
2530
+ continue;
2531
+ }
2532
+ try {
2533
+ await handler.removeServer(serverName);
2534
+ p8.log.success(`Removed from ${clientDisplayName(clientType)}`);
2535
+ } catch (err) {
2536
+ const msg = err instanceof Error ? err.message : String(err);
2537
+ errors.push(`${clientDisplayName(clientType)}: ${msg}`);
2538
+ }
2539
+ }
2540
+ if (errors.length > 0) {
2541
+ for (const e of errors) p8.log.error(e);
2542
+ p8.outro(import_picocolors7.default.red("Completed with errors."));
2543
+ process.exit(1);
2544
+ }
2545
+ p8.outro(import_picocolors7.default.green(`Removed "${serverName}" successfully.`));
2546
+ }
2547
+ });
2548
+
2549
+ // src/commands/run.ts
2550
+ init_cjs_shims();
2551
+ var import_citty9 = require("citty");
2552
+ var import_node_child_process3 = require("child_process");
2553
+ var import_picocolors8 = __toESM(require("picocolors"), 1);
2554
+ init_vault_service();
2555
+ var run_default = (0, import_citty9.defineCommand)({
2556
+ meta: {
2557
+ name: "run",
2558
+ description: "Run an installed MCP server with vault secrets injected"
2559
+ },
2560
+ args: {
2561
+ server: {
2562
+ type: "positional",
2563
+ description: "Server name to run (as installed in lockfile)",
2564
+ required: true
2565
+ },
2566
+ env: {
2567
+ type: "string",
2568
+ description: "Override env var KEY=VAL (repeatable)",
2569
+ alias: "e"
2570
+ }
2571
+ },
2572
+ async run({ args }) {
2573
+ const serverName = args.server;
2574
+ const lockfile = readLockfile();
2575
+ const entry = lockfile.servers[serverName];
2576
+ if (!entry) {
2577
+ console.error(import_picocolors8.default.red(` Error: Server '${serverName}' is not installed.`));
2578
+ console.error(import_picocolors8.default.dim(` Run ${import_picocolors8.default.cyan("mcpman install <server>")} to install it first.`));
2579
+ process.exit(1);
2580
+ }
2581
+ const lockfileEnv = parseEnvFlags(entry.envVars);
2582
+ const vaultEnv = await loadVaultSecrets(serverName);
2583
+ const cliEnv = parseEnvFlags(args.env);
2584
+ const finalEnv = {
2585
+ ...process.env,
2586
+ ...lockfileEnv,
2587
+ ...vaultEnv,
2588
+ ...cliEnv
2589
+ };
2590
+ console.log(import_picocolors8.default.dim(` Running ${import_picocolors8.default.cyan(serverName)}...`));
2591
+ const child = (0, import_node_child_process3.spawn)(entry.command, entry.args, {
2592
+ env: finalEnv,
2593
+ stdio: "inherit"
2594
+ });
2595
+ const forwardSignal = (signal) => {
2596
+ if (!child.killed) {
2597
+ child.kill(signal);
2598
+ }
2599
+ };
2600
+ process.on("SIGINT", () => forwardSignal("SIGINT"));
2601
+ process.on("SIGTERM", () => forwardSignal("SIGTERM"));
2602
+ await new Promise((resolve) => {
2603
+ child.on("close", (code) => {
2604
+ process.exit(code ?? 0);
2605
+ resolve();
2606
+ });
2607
+ child.on("error", (err) => {
2608
+ console.error(import_picocolors8.default.red(` Failed to start '${serverName}': ${err.message}`));
2609
+ process.exit(1);
2610
+ resolve();
2611
+ });
2612
+ });
2613
+ }
2614
+ });
2615
+ async function loadVaultSecrets(serverName) {
2616
+ try {
2617
+ const entries = listSecrets(serverName);
2618
+ if (entries.length === 0 || entries[0].keys.length === 0) {
2619
+ return {};
2620
+ }
2621
+ const password2 = await getMasterPassword();
2622
+ return getSecretsForServer(serverName, password2);
2623
+ } catch {
2624
+ console.warn(import_picocolors8.default.yellow(" Warning: Could not load vault secrets, continuing without them."));
2625
+ return {};
2626
+ }
2627
+ }
2628
+
2629
+ // src/commands/search.ts
2630
+ init_cjs_shims();
2631
+ var import_citty10 = require("citty");
2632
+ var import_picocolors9 = __toESM(require("picocolors"), 1);
2633
+ var import_nanospinner3 = require("nanospinner");
2634
+
2635
+ // src/core/registry-search.ts
2636
+ init_cjs_shims();
2637
+ var SEARCH_TIMEOUT_MS = 1e4;
2638
+ async function searchNpm(query, limit = 20) {
2639
+ const cap = Math.min(limit, 100);
2640
+ const url = `https://registry.npmjs.org/-/v1/search?text=mcp+${encodeURIComponent(query)}&size=${cap}`;
2641
+ try {
2642
+ const res = await fetch(url, { signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS) });
2643
+ if (!res.ok) return [];
2644
+ const data = await res.json();
2645
+ const objects = Array.isArray(data["objects"]) ? data["objects"] : [];
2646
+ return objects.map((obj) => {
2647
+ const pkg = obj["package"] ?? {};
2648
+ const dl = obj["downloads"];
2649
+ return {
2650
+ name: typeof pkg["name"] === "string" ? pkg["name"] : "",
2651
+ description: typeof pkg["description"] === "string" ? pkg["description"] : "",
2652
+ version: typeof pkg["version"] === "string" ? pkg["version"] : "",
2653
+ date: typeof pkg["date"] === "string" ? pkg["date"] : "",
2654
+ downloads: typeof dl?.["weekly"] === "number" ? dl["weekly"] : 0,
2655
+ keywords: Array.isArray(pkg["keywords"]) ? pkg["keywords"] : []
2656
+ };
2657
+ }).filter((r) => r.name !== "");
2658
+ } catch {
2659
+ return [];
2660
+ }
2661
+ }
2662
+ async function searchSmithery(query, limit = 20) {
2663
+ const cap = Math.min(limit, 100);
2664
+ const url = `https://api.smithery.ai/v1/servers?q=${encodeURIComponent(query)}&limit=${cap}`;
2665
+ try {
2666
+ const res = await fetch(url, { signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS) });
2667
+ if (!res.ok) return [];
2668
+ const data = await res.json();
2669
+ const servers = Array.isArray(data["servers"]) ? data["servers"] : [];
2670
+ return servers.map((s) => ({
2671
+ name: typeof s["name"] === "string" ? s["name"] : "",
2672
+ description: typeof s["description"] === "string" ? s["description"] : "",
2673
+ version: typeof s["version"] === "string" ? s["version"] : "latest",
2674
+ runtime: typeof s["runtime"] === "string" ? s["runtime"] : "node"
2675
+ })).filter((r) => r.name !== "");
2676
+ } catch {
2677
+ return [];
2678
+ }
2679
+ }
2680
+
2681
+ // src/commands/search.ts
2682
+ function truncate2(s, max) {
2683
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
2684
+ }
2685
+ function pad2(s, width) {
2686
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
2687
+ }
2688
+ function highlightMatch(name, query) {
2689
+ const idx = name.toLowerCase().indexOf(query.toLowerCase());
2690
+ if (idx === -1) return name;
2691
+ return name.slice(0, idx) + import_picocolors9.default.yellow(name.slice(idx, idx + query.length)) + name.slice(idx + query.length);
2692
+ }
2693
+ function formatDownloads(n) {
2694
+ if (!n) return import_picocolors9.default.dim("\u2014");
2695
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
2696
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
2697
+ return String(n);
2698
+ }
2699
+ function printNpmResults(results, query) {
2700
+ const nameWidth = Math.max(4, ...results.map((r) => r.name.length), 20);
2701
+ const verWidth = Math.max(7, ...results.map((r) => r.version.length));
2702
+ const dlWidth = 9;
2703
+ const descMax = 50;
2704
+ const header = ` ${pad2("NAME", nameWidth)} ${pad2("VERSION", verWidth)} ${pad2("DOWNLOADS", dlWidth)} DESCRIPTION`;
2705
+ console.log(import_picocolors9.default.dim(header));
2706
+ console.log(import_picocolors9.default.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(verWidth)} ${"-".repeat(dlWidth)} ${"-".repeat(descMax)}`));
2707
+ for (const r of results) {
2708
+ const name = highlightMatch(pad2(r.name, nameWidth), query);
2709
+ const ver = pad2(r.version, verWidth);
2710
+ const dl = pad2(formatDownloads(r.downloads), dlWidth);
2711
+ const desc = truncate2(r.description || import_picocolors9.default.dim("(no description)"), descMax);
2712
+ console.log(` ${name} ${import_picocolors9.default.dim(ver)} ${dl} ${desc}`);
2713
+ }
2714
+ }
2715
+ function printSmitheryResults(results, query) {
2716
+ const nameWidth = Math.max(4, ...results.map((r) => r.name.length), 20);
2717
+ const verWidth = Math.max(7, ...results.map((r) => r.version.length));
2718
+ const descMax = 50;
2719
+ const header = ` ${pad2("NAME", nameWidth)} ${pad2("VERSION", verWidth)} DESCRIPTION`;
2720
+ console.log(import_picocolors9.default.dim(header));
2721
+ console.log(import_picocolors9.default.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(verWidth)} ${"-".repeat(descMax)}`));
2722
+ for (const r of results) {
2723
+ const name = highlightMatch(pad2(r.name, nameWidth), query);
2724
+ const ver = pad2(r.version, verWidth);
2725
+ const desc = truncate2(r.description || import_picocolors9.default.dim("(no description)"), descMax);
2726
+ console.log(` ${name} ${import_picocolors9.default.dim(ver)} ${desc}`);
2727
+ }
2728
+ }
2729
+ var search_default = (0, import_citty10.defineCommand)({
1821
2730
  meta: {
1822
- name: "remove",
1823
- description: "Remove an MCP server from one or more AI clients"
2731
+ name: "search",
2732
+ description: "Search for MCP servers on npm or Smithery registry"
1824
2733
  },
1825
2734
  args: {
1826
- server: {
2735
+ query: {
1827
2736
  type: "positional",
1828
- description: "Server name to remove",
2737
+ description: "Search query",
1829
2738
  required: true
1830
2739
  },
1831
- client: {
2740
+ registry: {
1832
2741
  type: "string",
1833
- description: "Target client (claude, cursor, vscode, windsurf)"
1834
- },
1835
- all: {
1836
- type: "boolean",
1837
- description: "Remove from all clients",
1838
- default: false
2742
+ description: "Registry to search: npm or smithery (default: npm)",
2743
+ default: "npm"
1839
2744
  },
1840
- yes: {
1841
- type: "boolean",
1842
- description: "Skip confirmation prompt",
1843
- default: false
2745
+ limit: {
2746
+ type: "string",
2747
+ description: "Maximum number of results (default: 20, max: 100)",
2748
+ default: "20"
1844
2749
  }
1845
2750
  },
1846
2751
  async run({ args }) {
1847
- p4.intro(import_picocolors5.default.bold("mcpman remove"));
1848
- const serverName = args.server;
1849
- const servers = await getInstalledServers();
1850
- const match = servers.find((s) => s.name === serverName);
1851
- if (!match) {
1852
- p4.log.warn(`Server "${serverName}" is not installed.`);
1853
- const similar = servers.filter((s) => s.name.includes(serverName) || serverName.includes(s.name));
1854
- if (similar.length > 0) {
1855
- p4.log.info(`Did you mean: ${similar.map((s) => import_picocolors5.default.cyan(s.name)).join(", ")}?`);
1856
- }
1857
- p4.outro("Nothing to remove.");
1858
- return;
1859
- }
1860
- let targetClients;
1861
- if (args.all) {
1862
- targetClients = match.clients;
1863
- } else if (args.client) {
1864
- if (!match.clients.includes(args.client)) {
1865
- p4.log.warn(`Server "${serverName}" is not installed in client "${args.client}".`);
1866
- p4.outro("Nothing to remove.");
1867
- return;
1868
- }
1869
- targetClients = [args.client];
1870
- } else if (match.clients.length === 1) {
1871
- targetClients = match.clients;
1872
- } else {
1873
- const selected = await p4.multiselect({
1874
- message: `Remove "${serverName}" from which clients?`,
1875
- options: match.clients.map((c) => ({
1876
- value: c,
1877
- label: clientDisplayName(c)
1878
- })),
1879
- required: true
1880
- });
1881
- if (p4.isCancel(selected)) {
1882
- p4.outro("Cancelled.");
1883
- process.exit(0);
1884
- }
1885
- targetClients = selected;
2752
+ const query = args.query;
2753
+ const registry = args.registry.toLowerCase();
2754
+ const limit = Math.min(Math.max(1, Number.parseInt(args.limit, 10) || 20), 100);
2755
+ if (registry !== "npm" && registry !== "smithery") {
2756
+ console.error(import_picocolors9.default.red(` Unknown registry "${registry}". Use "npm" or "smithery".`));
2757
+ process.exit(1);
1886
2758
  }
1887
- if (!args.yes) {
1888
- const clientNames = targetClients.map(clientDisplayName).join(", ");
1889
- const confirmed = await p4.confirm({
1890
- message: `Remove ${import_picocolors5.default.cyan(serverName)} from ${import_picocolors5.default.yellow(clientNames)}?`
1891
- });
1892
- if (p4.isCancel(confirmed) || !confirmed) {
1893
- p4.outro("Cancelled.");
2759
+ const spinner5 = (0, import_nanospinner3.createSpinner)(`Searching ${registry} for "${query}"...`).start();
2760
+ if (registry === "npm") {
2761
+ const results2 = await searchNpm(query, limit);
2762
+ spinner5.stop();
2763
+ if (results2.length === 0) {
2764
+ console.log(import_picocolors9.default.dim(`
2765
+ No results found for "${query}" on npm.
2766
+ `));
1894
2767
  return;
1895
2768
  }
2769
+ console.log(import_picocolors9.default.bold(`
2770
+ mcpman search \u2014 npm (${results2.length} result${results2.length !== 1 ? "s" : ""})
2771
+ `));
2772
+ printNpmResults(results2, query);
2773
+ console.log(import_picocolors9.default.dim(`
2774
+ Install with: mcpman install <name>
2775
+ `));
2776
+ return;
1896
2777
  }
1897
- const installedClients = await getInstalledClients();
1898
- const errors = [];
1899
- for (const clientType of targetClients) {
1900
- const handler = installedClients.find((c) => c.type === clientType);
1901
- if (!handler) {
1902
- errors.push(`Client "${clientType}" not found`);
1903
- continue;
1904
- }
1905
- try {
1906
- await handler.removeServer(serverName);
1907
- p4.log.success(`Removed from ${clientDisplayName(clientType)}`);
1908
- } catch (err) {
1909
- const msg = err instanceof Error ? err.message : String(err);
1910
- errors.push(`${clientDisplayName(clientType)}: ${msg}`);
1911
- }
1912
- }
1913
- if (errors.length > 0) {
1914
- for (const e of errors) p4.log.error(e);
1915
- p4.outro(import_picocolors5.default.red("Completed with errors."));
1916
- process.exit(1);
2778
+ const results = await searchSmithery(query, limit);
2779
+ spinner5.stop();
2780
+ if (results.length === 0) {
2781
+ console.log(import_picocolors9.default.dim(`
2782
+ No results found for "${query}" on Smithery.
2783
+ `));
2784
+ return;
1917
2785
  }
1918
- p4.outro(import_picocolors5.default.green(`Removed "${serverName}" successfully.`));
2786
+ console.log(import_picocolors9.default.bold(`
2787
+ mcpman search \u2014 Smithery (${results.length} result${results.length !== 1 ? "s" : ""})
2788
+ `));
2789
+ printSmitheryResults(results, query);
2790
+ console.log(import_picocolors9.default.dim(`
2791
+ Install with: mcpman install <name>
2792
+ `));
1919
2793
  }
1920
2794
  });
1921
2795
 
1922
2796
  // src/commands/secrets.ts
1923
2797
  init_cjs_shims();
1924
- var import_citty7 = require("citty");
1925
- var import_picocolors6 = __toESM(require("picocolors"), 1);
1926
- var p6 = __toESM(require("@clack/prompts"), 1);
2798
+ var import_citty11 = require("citty");
2799
+ var import_picocolors10 = __toESM(require("picocolors"), 1);
2800
+ var p9 = __toESM(require("@clack/prompts"), 1);
1927
2801
  init_vault_service();
1928
2802
  function maskValue(value) {
1929
2803
  if (value.length <= 8) return "***";
@@ -1934,7 +2808,7 @@ function parseKeyValue(input) {
1934
2808
  if (idx <= 0) return null;
1935
2809
  return { key: input.slice(0, idx), value: input.slice(idx + 1) };
1936
2810
  }
1937
- var setCommand = (0, import_citty7.defineCommand)({
2811
+ var setCommand2 = (0, import_citty11.defineCommand)({
1938
2812
  meta: { name: "set", description: "Store an encrypted secret for a server" },
1939
2813
  args: {
1940
2814
  server: {
@@ -1951,30 +2825,30 @@ var setCommand = (0, import_citty7.defineCommand)({
1951
2825
  async run({ args }) {
1952
2826
  const parsed = parseKeyValue(args.keyvalue);
1953
2827
  if (!parsed) {
1954
- console.error(import_picocolors6.default.red("\u2717") + " Invalid format. Expected KEY=VALUE");
2828
+ console.error(import_picocolors10.default.red("\u2717") + " Invalid format. Expected KEY=VALUE");
1955
2829
  process.exit(1);
1956
2830
  }
1957
- p6.intro(import_picocolors6.default.cyan("mcpman secrets set"));
2831
+ p9.intro(import_picocolors10.default.cyan("mcpman secrets set"));
1958
2832
  const isNew = listSecrets(args.server).length === 0 || !listSecrets(args.server)[0]?.keys.includes(parsed.key);
1959
2833
  const vaultPath = (await Promise.resolve().then(() => (init_vault_service(), vault_service_exports))).getVaultPath();
1960
2834
  const vaultExists = (await import("fs")).existsSync(vaultPath);
1961
2835
  const password2 = await getMasterPassword(!vaultExists && isNew);
1962
- const spin = p6.spinner();
2836
+ const spin = p9.spinner();
1963
2837
  spin.start("Encrypting secret...");
1964
2838
  try {
1965
2839
  setSecret(args.server, parsed.key, parsed.value, password2);
1966
2840
  spin.stop(
1967
- `${import_picocolors6.default.green("\u2713")} Stored ${import_picocolors6.default.bold(parsed.key)} for ${import_picocolors6.default.cyan(args.server)}`
2841
+ `${import_picocolors10.default.green("\u2713")} Stored ${import_picocolors10.default.bold(parsed.key)} for ${import_picocolors10.default.cyan(args.server)}`
1968
2842
  );
1969
2843
  } catch (err) {
1970
- spin.stop(import_picocolors6.default.red("\u2717") + " Failed to store secret");
1971
- console.error(import_picocolors6.default.dim(String(err)));
2844
+ spin.stop(import_picocolors10.default.red("\u2717") + " Failed to store secret");
2845
+ console.error(import_picocolors10.default.dim(String(err)));
1972
2846
  process.exit(1);
1973
2847
  }
1974
- p6.outro(import_picocolors6.default.dim("Secret encrypted and saved to vault."));
2848
+ p9.outro(import_picocolors10.default.dim("Secret encrypted and saved to vault."));
1975
2849
  }
1976
2850
  });
1977
- var listCommand = (0, import_citty7.defineCommand)({
2851
+ var listCommand2 = (0, import_citty11.defineCommand)({
1978
2852
  meta: { name: "list", description: "List secret keys stored in the vault" },
1979
2853
  args: {
1980
2854
  server: {
@@ -1986,23 +2860,23 @@ var listCommand = (0, import_citty7.defineCommand)({
1986
2860
  async run({ args }) {
1987
2861
  const results = listSecrets(args.server || void 0);
1988
2862
  if (results.length === 0) {
1989
- const filter = args.server ? ` for ${import_picocolors6.default.cyan(args.server)}` : "";
1990
- console.log(import_picocolors6.default.dim(`No secrets stored${filter}.`));
2863
+ const filter = args.server ? ` for ${import_picocolors10.default.cyan(args.server)}` : "";
2864
+ console.log(import_picocolors10.default.dim(`No secrets stored${filter}.`));
1991
2865
  return;
1992
2866
  }
1993
2867
  console.log("");
1994
2868
  for (const { server, keys } of results) {
1995
- console.log(import_picocolors6.default.bold(import_picocolors6.default.cyan(server)));
2869
+ console.log(import_picocolors10.default.bold(import_picocolors10.default.cyan(server)));
1996
2870
  for (const key of keys) {
1997
- console.log(` ${import_picocolors6.default.green("\u25CF")} ${import_picocolors6.default.bold(key)} ${import_picocolors6.default.dim(maskValue("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"))}`);
2871
+ console.log(` ${import_picocolors10.default.green("\u25CF")} ${import_picocolors10.default.bold(key)} ${import_picocolors10.default.dim(maskValue("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"))}`);
1998
2872
  }
1999
2873
  console.log("");
2000
2874
  }
2001
2875
  const total = results.reduce((n, r) => n + r.keys.length, 0);
2002
- console.log(import_picocolors6.default.dim(` ${total} secret${total !== 1 ? "s" : ""} in ${results.length} server${results.length !== 1 ? "s" : ""}`));
2876
+ console.log(import_picocolors10.default.dim(` ${total} secret${total !== 1 ? "s" : ""} in ${results.length} server${results.length !== 1 ? "s" : ""}`));
2003
2877
  }
2004
2878
  });
2005
- var removeCommand = (0, import_citty7.defineCommand)({
2879
+ var removeCommand = (0, import_citty11.defineCommand)({
2006
2880
  meta: { name: "remove", description: "Delete a secret from the vault" },
2007
2881
  args: {
2008
2882
  server: {
@@ -2017,41 +2891,41 @@ var removeCommand = (0, import_citty7.defineCommand)({
2017
2891
  }
2018
2892
  },
2019
2893
  async run({ args }) {
2020
- const confirmed = await p6.confirm({
2021
- message: `Remove ${import_picocolors6.default.bold(args.key)} from ${import_picocolors6.default.cyan(args.server)}?`,
2894
+ const confirmed = await p9.confirm({
2895
+ message: `Remove ${import_picocolors10.default.bold(args.key)} from ${import_picocolors10.default.cyan(args.server)}?`,
2022
2896
  initialValue: false
2023
2897
  });
2024
- if (p6.isCancel(confirmed) || !confirmed) {
2025
- p6.cancel("Cancelled.");
2898
+ if (p9.isCancel(confirmed) || !confirmed) {
2899
+ p9.cancel("Cancelled.");
2026
2900
  return;
2027
2901
  }
2028
2902
  try {
2029
2903
  removeSecret(args.server, args.key);
2030
- console.log(`${import_picocolors6.default.green("\u2713")} Removed ${import_picocolors6.default.bold(args.key)} from ${import_picocolors6.default.cyan(args.server)}`);
2904
+ console.log(`${import_picocolors10.default.green("\u2713")} Removed ${import_picocolors10.default.bold(args.key)} from ${import_picocolors10.default.cyan(args.server)}`);
2031
2905
  } catch (err) {
2032
- console.error(import_picocolors6.default.red("\u2717") + " Failed to remove secret");
2033
- console.error(import_picocolors6.default.dim(String(err)));
2906
+ console.error(import_picocolors10.default.red("\u2717") + " Failed to remove secret");
2907
+ console.error(import_picocolors10.default.dim(String(err)));
2034
2908
  process.exit(1);
2035
2909
  }
2036
2910
  }
2037
2911
  });
2038
- var secrets_default = (0, import_citty7.defineCommand)({
2912
+ var secrets_default = (0, import_citty11.defineCommand)({
2039
2913
  meta: {
2040
2914
  name: "secrets",
2041
2915
  description: "Manage encrypted secrets for MCP servers"
2042
2916
  },
2043
2917
  subCommands: {
2044
- set: setCommand,
2045
- list: listCommand,
2918
+ set: setCommand2,
2919
+ list: listCommand2,
2046
2920
  remove: removeCommand
2047
2921
  }
2048
2922
  });
2049
2923
 
2050
2924
  // src/commands/sync.ts
2051
2925
  init_cjs_shims();
2052
- var import_citty8 = require("citty");
2053
- var p7 = __toESM(require("@clack/prompts"), 1);
2054
- var import_picocolors7 = __toESM(require("picocolors"), 1);
2926
+ var import_citty12 = require("citty");
2927
+ var p10 = __toESM(require("@clack/prompts"), 1);
2928
+ var import_picocolors11 = __toESM(require("picocolors"), 1);
2055
2929
 
2056
2930
  // src/core/config-diff.ts
2057
2931
  init_cjs_shims();
@@ -2067,7 +2941,7 @@ function reconstructServerEntry(lockEntry) {
2067
2941
  }
2068
2942
  return entry;
2069
2943
  }
2070
- function computeDiff(lockfile, clientConfigs) {
2944
+ function computeDiff(lockfile, clientConfigs, options = {}) {
2071
2945
  const actions = [];
2072
2946
  for (const [server, lockEntry] of Object.entries(lockfile.servers)) {
2073
2947
  for (const client of lockEntry.clients) {
@@ -2085,19 +2959,21 @@ function computeDiff(lockfile, clientConfigs) {
2085
2959
  }
2086
2960
  }
2087
2961
  }
2962
+ const extraAction = options.remove ? "remove" : "extra";
2088
2963
  for (const [client, config] of clientConfigs) {
2089
2964
  for (const server of Object.keys(config.servers)) {
2090
2965
  if (!(server in lockfile.servers)) {
2091
- actions.push({ server, client, action: "extra" });
2966
+ actions.push({ server, client, action: extraAction });
2092
2967
  }
2093
2968
  }
2094
2969
  }
2095
2970
  return actions;
2096
2971
  }
2097
- function computeDiffFromClient(sourceClient, clientConfigs) {
2972
+ function computeDiffFromClient(sourceClient, clientConfigs, options = {}) {
2098
2973
  const actions = [];
2099
2974
  const sourceConfig = clientConfigs.get(sourceClient);
2100
2975
  if (!sourceConfig) return [];
2976
+ const extraAction = options.remove ? "remove" : "extra";
2101
2977
  for (const [client, config] of clientConfigs) {
2102
2978
  if (client === sourceClient) continue;
2103
2979
  for (const [server, entry] of Object.entries(sourceConfig.servers)) {
@@ -2109,7 +2985,7 @@ function computeDiffFromClient(sourceClient, clientConfigs) {
2109
2985
  }
2110
2986
  for (const server of Object.keys(config.servers)) {
2111
2987
  if (!(server in sourceConfig.servers)) {
2112
- actions.push({ server, client, action: "extra" });
2988
+ actions.push({ server, client, action: extraAction });
2113
2989
  }
2114
2990
  }
2115
2991
  }
@@ -2120,7 +2996,7 @@ function computeDiffFromClient(sourceClient, clientConfigs) {
2120
2996
  init_cjs_shims();
2121
2997
  init_client_detector();
2122
2998
  async function applySyncActions(actions, clients) {
2123
- const result = { applied: 0, failed: 0, errors: [] };
2999
+ const result = { applied: 0, removed: 0, failed: 0, errors: [] };
2124
3000
  const addActions = actions.filter((a) => a.action === "add" && a.entry);
2125
3001
  for (const action of addActions) {
2126
3002
  const handler = clients.get(action.client);
@@ -2145,6 +3021,30 @@ async function applySyncActions(actions, clients) {
2145
3021
  });
2146
3022
  }
2147
3023
  }
3024
+ const removeActions = actions.filter((a) => a.action === "remove");
3025
+ for (const action of removeActions) {
3026
+ const handler = clients.get(action.client);
3027
+ if (!handler) {
3028
+ result.failed++;
3029
+ result.errors.push({
3030
+ server: action.server,
3031
+ client: action.client,
3032
+ error: "No handler available for client"
3033
+ });
3034
+ continue;
3035
+ }
3036
+ try {
3037
+ await handler.removeServer(action.server);
3038
+ result.removed++;
3039
+ } catch (err) {
3040
+ result.failed++;
3041
+ result.errors.push({
3042
+ server: action.server,
3043
+ client: action.client,
3044
+ error: String(err)
3045
+ });
3046
+ }
3047
+ }
2148
3048
  return result;
2149
3049
  }
2150
3050
  async function getClientConfigs() {
@@ -2178,7 +3078,7 @@ var CLIENT_DISPLAY3 = {
2178
3078
  vscode: "VS Code",
2179
3079
  windsurf: "Windsurf"
2180
3080
  };
2181
- var sync_default = (0, import_citty8.defineCommand)({
3081
+ var sync_default = (0, import_citty12.defineCommand)({
2182
3082
  meta: {
2183
3083
  name: "sync",
2184
3084
  description: "Sync MCP server configs across all detected AI clients"
@@ -2189,6 +3089,11 @@ var sync_default = (0, import_citty8.defineCommand)({
2189
3089
  description: "Preview changes without applying them",
2190
3090
  default: false
2191
3091
  },
3092
+ remove: {
3093
+ type: "boolean",
3094
+ description: "Remove extra servers not in lockfile",
3095
+ default: false
3096
+ },
2192
3097
  source: {
2193
3098
  type: "string",
2194
3099
  description: "Use a specific client as source of truth (claude-desktop, cursor, vscode, windsurf)"
@@ -2200,58 +3105,64 @@ var sync_default = (0, import_citty8.defineCommand)({
2200
3105
  }
2201
3106
  },
2202
3107
  async run({ args }) {
2203
- p7.intro(`${import_picocolors7.default.cyan("mcpman sync")}`);
3108
+ p10.intro(`${import_picocolors11.default.cyan("mcpman sync")}`);
2204
3109
  const sourceClient = args.source;
2205
3110
  if (sourceClient && !VALID_CLIENTS.includes(sourceClient)) {
2206
- p7.log.error(`Invalid --source "${sourceClient}". Must be one of: ${VALID_CLIENTS.join(", ")}`);
3111
+ p10.log.error(`Invalid --source "${sourceClient}". Must be one of: ${VALID_CLIENTS.join(", ")}`);
2207
3112
  process.exit(1);
2208
3113
  }
2209
- const spinner5 = p7.spinner();
3114
+ const spinner5 = p10.spinner();
2210
3115
  spinner5.start("Detecting clients and reading configs...");
2211
3116
  const { configs, handlers } = await getClientConfigs();
2212
3117
  spinner5.stop(`Found ${configs.size} client(s)`);
2213
3118
  if (configs.size === 0) {
2214
- p7.log.warn("No AI clients detected. Install Claude Desktop, Cursor, VS Code, or Windsurf first.");
3119
+ p10.log.warn("No AI clients detected. Install Claude Desktop, Cursor, VS Code, or Windsurf first.");
2215
3120
  process.exit(0);
2216
3121
  }
3122
+ const diffOptions = { remove: args.remove };
2217
3123
  let actions;
2218
3124
  if (sourceClient) {
2219
3125
  if (!configs.has(sourceClient)) {
2220
- p7.log.error(`Source client "${sourceClient}" is not detected or its config is unreadable.`);
3126
+ p10.log.error(`Source client "${sourceClient}" is not detected or its config is unreadable.`);
2221
3127
  process.exit(1);
2222
3128
  }
2223
- p7.log.info(`Using ${CLIENT_DISPLAY3[sourceClient]} as source of truth`);
2224
- actions = computeDiffFromClient(sourceClient, configs);
3129
+ p10.log.info(`Using ${CLIENT_DISPLAY3[sourceClient]} as source of truth`);
3130
+ actions = computeDiffFromClient(sourceClient, configs, diffOptions);
2225
3131
  } else {
2226
3132
  const lockfile = readLockfile();
2227
- actions = computeDiff(lockfile, configs);
3133
+ actions = computeDiff(lockfile, configs, diffOptions);
2228
3134
  }
2229
3135
  printDiffTable(actions);
2230
3136
  const addCount = actions.filter((a) => a.action === "add").length;
2231
3137
  const extraCount = actions.filter((a) => a.action === "extra").length;
2232
- if (addCount === 0 && extraCount === 0) {
2233
- p7.outro(import_picocolors7.default.green("All clients are in sync."));
3138
+ const removeCount = actions.filter((a) => a.action === "remove").length;
3139
+ if (addCount === 0 && removeCount === 0 && extraCount === 0) {
3140
+ p10.outro(import_picocolors11.default.green("All clients are in sync."));
2234
3141
  process.exit(0);
2235
3142
  }
2236
3143
  const parts = [];
2237
- if (addCount > 0) parts.push(import_picocolors7.default.green(`${addCount} to add`));
2238
- if (extraCount > 0) parts.push(import_picocolors7.default.yellow(`${extraCount} extra (informational)`));
2239
- p7.log.info(parts.join(" \xB7 "));
3144
+ if (addCount > 0) parts.push(import_picocolors11.default.green(`${addCount} to add`));
3145
+ if (removeCount > 0) parts.push(import_picocolors11.default.red(`${removeCount} to remove`));
3146
+ if (extraCount > 0) parts.push(import_picocolors11.default.yellow(`${extraCount} extra (informational)`));
3147
+ p10.log.info(parts.join(" \xB7 "));
2240
3148
  if (args["dry-run"]) {
2241
- p7.outro(import_picocolors7.default.dim("Dry run \u2014 no changes applied."));
3149
+ p10.outro(import_picocolors11.default.dim("Dry run \u2014 no changes applied."));
2242
3150
  process.exit(1);
2243
3151
  }
2244
- if (addCount === 0) {
2245
- p7.outro(import_picocolors7.default.dim("No additions needed. Extra servers left untouched."));
3152
+ if (addCount === 0 && removeCount === 0) {
3153
+ p10.outro(import_picocolors11.default.dim("No additions needed. Extra servers left untouched."));
2246
3154
  process.exit(1);
2247
3155
  }
2248
3156
  if (!args.yes) {
2249
- const confirmed = await p7.confirm({
2250
- message: `Apply ${addCount} addition(s) to client configs?`,
3157
+ const actionParts = [];
3158
+ if (addCount > 0) actionParts.push(`${addCount} addition(s)`);
3159
+ if (removeCount > 0) actionParts.push(`${removeCount} removal(s)`);
3160
+ const confirmed = await p10.confirm({
3161
+ message: `Apply ${actionParts.join(" and ")} to client configs?`,
2251
3162
  initialValue: true
2252
3163
  });
2253
- if (p7.isCancel(confirmed) || !confirmed) {
2254
- p7.outro(import_picocolors7.default.dim("Cancelled \u2014 no changes applied."));
3164
+ if (p10.isCancel(confirmed) || !confirmed) {
3165
+ p10.outro(import_picocolors11.default.dim("Cancelled \u2014 no changes applied."));
2255
3166
  process.exit(0);
2256
3167
  }
2257
3168
  }
@@ -2259,196 +3170,80 @@ var sync_default = (0, import_citty8.defineCommand)({
2259
3170
  const result = await applySyncActions(actions, handlers);
2260
3171
  spinner5.stop("Done");
2261
3172
  if (result.applied > 0) {
2262
- p7.log.success(`Added ${result.applied} server(s) to client configs.`);
3173
+ p10.log.success(`Added ${result.applied} server(s) to client configs.`);
3174
+ }
3175
+ if (result.removed > 0) {
3176
+ p10.log.success(`Removed ${result.removed} server(s) from client configs.`);
2263
3177
  }
2264
3178
  if (result.failed > 0) {
2265
3179
  for (const e of result.errors) {
2266
- p7.log.error(`Failed to add "${e.server}" to ${e.client}: ${e.error}`);
3180
+ p10.log.error(`Failed to sync "${e.server}" on ${e.client}: ${e.error}`);
2267
3181
  }
2268
3182
  }
2269
- p7.outro(result.failed === 0 ? import_picocolors7.default.green("Sync complete.") : import_picocolors7.default.yellow("Sync complete with errors."));
3183
+ p10.outro(result.failed === 0 ? import_picocolors11.default.green("Sync complete.") : import_picocolors11.default.yellow("Sync complete with errors."));
2270
3184
  process.exit(result.failed > 0 ? 1 : 0);
2271
3185
  }
2272
3186
  });
2273
3187
  function printDiffTable(actions) {
2274
3188
  if (actions.length === 0) {
2275
- p7.log.info("No actions to display.");
3189
+ p10.log.info("No actions to display.");
2276
3190
  return;
2277
3191
  }
2278
3192
  const nameWidth = Math.max(6, ...actions.map((a) => a.server.length));
2279
3193
  const clientWidth = Math.max(6, ...actions.map((a) => CLIENT_DISPLAY3[a.client]?.length ?? a.client.length));
2280
- const header = ` ${pad2("SERVER", nameWidth)} ${pad2("CLIENT", clientWidth)} STATUS`;
2281
- console.log(import_picocolors7.default.dim(header));
2282
- console.log(import_picocolors7.default.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientWidth)} ------`));
3194
+ const header = ` ${pad3("SERVER", nameWidth)} ${pad3("CLIENT", clientWidth)} STATUS`;
3195
+ console.log(import_picocolors11.default.dim(header));
3196
+ console.log(import_picocolors11.default.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientWidth)} ------`));
2283
3197
  for (const action of actions) {
2284
3198
  const clientDisplay = CLIENT_DISPLAY3[action.client] ?? action.client;
2285
3199
  const [icon, statusText] = formatAction(action.action);
2286
- console.log(` ${pad2(action.server, nameWidth)} ${pad2(clientDisplay, clientWidth)} ${icon} ${statusText}`);
3200
+ console.log(` ${pad3(action.server, nameWidth)} ${pad3(clientDisplay, clientWidth)} ${icon} ${statusText}`);
2287
3201
  }
2288
3202
  console.log("");
2289
3203
  }
2290
3204
  function formatAction(action) {
2291
3205
  switch (action) {
2292
3206
  case "add":
2293
- return [import_picocolors7.default.green("+"), import_picocolors7.default.green("missing \u2014 will add")];
3207
+ return [import_picocolors11.default.green("+"), import_picocolors11.default.green("missing \u2014 will add")];
2294
3208
  case "extra":
2295
- return [import_picocolors7.default.yellow("?"), import_picocolors7.default.yellow("extra (not in lockfile)")];
3209
+ return [import_picocolors11.default.yellow("?"), import_picocolors11.default.yellow("extra (not in lockfile)")];
3210
+ case "remove":
3211
+ return [import_picocolors11.default.red("\u2013"), import_picocolors11.default.red("extra \u2014 will remove")];
2296
3212
  case "ok":
2297
- return [import_picocolors7.default.dim("\xB7"), import_picocolors7.default.dim("in sync")];
3213
+ return [import_picocolors11.default.dim("\xB7"), import_picocolors11.default.dim("in sync")];
2298
3214
  }
2299
3215
  }
2300
- function pad2(s, width) {
3216
+ function pad3(s, width) {
2301
3217
  return s.length >= width ? s : s + " ".repeat(width - s.length);
2302
3218
  }
2303
3219
 
2304
3220
  // src/commands/update.ts
2305
3221
  init_cjs_shims();
2306
- var import_citty9 = require("citty");
2307
- var p8 = __toESM(require("@clack/prompts"), 1);
2308
- var import_picocolors9 = __toESM(require("picocolors"), 1);
2309
-
2310
- // src/core/version-checker.ts
2311
- init_cjs_shims();
2312
- function compareVersions(a, b) {
2313
- const aParts = a.replace(/^v/, "").split(".").map(Number);
2314
- const bParts = b.replace(/^v/, "").split(".").map(Number);
2315
- const len = Math.max(aParts.length, bParts.length);
2316
- for (let i = 0; i < len; i++) {
2317
- const aN = aParts[i] ?? 0;
2318
- const bN = bParts[i] ?? 0;
2319
- if (Number.isNaN(aN) || Number.isNaN(bN)) return 0;
2320
- if (aN < bN) return -1;
2321
- if (aN > bN) return 1;
2322
- }
2323
- return 0;
2324
- }
2325
- function detectUpdateType(current, latest) {
2326
- const cParts = current.replace(/^v/, "").split(".").map(Number);
2327
- const lParts = latest.replace(/^v/, "").split(".").map(Number);
2328
- if ((lParts[0] ?? 0) > (cParts[0] ?? 0)) return "major";
2329
- if ((lParts[1] ?? 0) > (cParts[1] ?? 0)) return "minor";
2330
- return "patch";
2331
- }
2332
- async function fetchNpmLatest(packageName) {
2333
- try {
2334
- const res = await fetch(
2335
- `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
2336
- {
2337
- headers: { Accept: "application/json" },
2338
- signal: AbortSignal.timeout(8e3)
2339
- }
2340
- );
2341
- if (!res.ok) return null;
2342
- const data = await res.json();
2343
- return typeof data.version === "string" ? data.version : null;
2344
- } catch {
2345
- return null;
2346
- }
2347
- }
2348
- async function fetchSmitheryLatest(name) {
2349
- try {
2350
- const res = await fetch(
2351
- `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`,
2352
- {
2353
- headers: { Accept: "application/json" },
2354
- signal: AbortSignal.timeout(8e3)
2355
- }
2356
- );
2357
- if (!res.ok) return null;
2358
- const data = await res.json();
2359
- return typeof data.version === "string" ? data.version : null;
2360
- } catch {
2361
- return null;
2362
- }
2363
- }
2364
- async function fetchGithubLatest(resolved) {
2365
- const match = resolved.match(/github\.com\/([^/]+)\/([^/]+)/);
2366
- if (!match) return null;
2367
- const [, owner, repo] = match;
2368
- try {
2369
- const res = await fetch(
2370
- `https://api.github.com/repos/${owner}/${repo}/releases/latest`,
2371
- {
2372
- headers: { Accept: "application/json" },
2373
- signal: AbortSignal.timeout(8e3)
2374
- }
2375
- );
2376
- if (!res.ok) return null;
2377
- const data = await res.json();
2378
- return typeof data.tag_name === "string" ? data.tag_name.replace(/^v/, "") : null;
2379
- } catch {
2380
- return null;
2381
- }
2382
- }
2383
- async function checkVersion(name, lockEntry) {
2384
- const current = lockEntry.version;
2385
- let latest = null;
2386
- if (lockEntry.source === "npm") {
2387
- latest = await fetchNpmLatest(name);
2388
- } else if (lockEntry.source === "smithery") {
2389
- latest = await fetchSmitheryLatest(name);
2390
- } else if (lockEntry.source === "github") {
2391
- latest = await fetchGithubLatest(lockEntry.resolved);
2392
- }
2393
- if (!latest || latest === current) {
2394
- return {
2395
- server: name,
2396
- source: lockEntry.source,
2397
- currentVersion: current,
2398
- latestVersion: latest ?? current,
2399
- hasUpdate: false
2400
- };
2401
- }
2402
- const hasUpdate = compareVersions(current, latest) === -1;
2403
- return {
2404
- server: name,
2405
- source: lockEntry.source,
2406
- currentVersion: current,
2407
- latestVersion: latest,
2408
- hasUpdate,
2409
- updateType: hasUpdate ? detectUpdateType(current, latest) : void 0
2410
- };
2411
- }
2412
- async function checkAllVersions(lockfile) {
2413
- const entries = Object.entries(lockfile.servers);
2414
- if (entries.length === 0) return [];
2415
- const results = [];
2416
- const executing = /* @__PURE__ */ new Set();
2417
- for (const [name, entry] of entries) {
2418
- const p9 = checkVersion(name, entry).then((r) => {
2419
- results.push(r);
2420
- executing.delete(p9);
2421
- });
2422
- executing.add(p9);
2423
- if (executing.size >= 5) {
2424
- await Promise.race(executing);
2425
- }
2426
- }
2427
- await Promise.all(executing);
2428
- return results;
2429
- }
3222
+ var import_citty13 = require("citty");
3223
+ var p11 = __toESM(require("@clack/prompts"), 1);
3224
+ var import_picocolors13 = __toESM(require("picocolors"), 1);
2430
3225
 
2431
3226
  // src/core/update-notifier.ts
2432
3227
  init_cjs_shims();
2433
- var import_node_fs5 = __toESM(require("fs"), 1);
2434
- var import_node_path7 = __toESM(require("path"), 1);
3228
+ var import_node_fs6 = __toESM(require("fs"), 1);
3229
+ var import_node_path8 = __toESM(require("path"), 1);
2435
3230
  var import_node_os5 = __toESM(require("os"), 1);
2436
- var import_picocolors8 = __toESM(require("picocolors"), 1);
2437
- var CACHE_FILE = import_node_path7.default.join(import_node_os5.default.homedir(), ".mcpman", ".update-check");
3231
+ var import_picocolors12 = __toESM(require("picocolors"), 1);
3232
+ var CACHE_FILE = import_node_path8.default.join(import_node_os5.default.homedir(), ".mcpman", ".update-check");
2438
3233
  var TTL_MS = 24 * 60 * 60 * 1e3;
2439
3234
  function writeUpdateCache(data) {
2440
3235
  try {
2441
- const dir = import_node_path7.default.dirname(CACHE_FILE);
2442
- if (!import_node_fs5.default.existsSync(dir)) import_node_fs5.default.mkdirSync(dir, { recursive: true });
3236
+ const dir = import_node_path8.default.dirname(CACHE_FILE);
3237
+ if (!import_node_fs6.default.existsSync(dir)) import_node_fs6.default.mkdirSync(dir, { recursive: true });
2443
3238
  const tmp = `${CACHE_FILE}.tmp`;
2444
- import_node_fs5.default.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
2445
- import_node_fs5.default.renameSync(tmp, CACHE_FILE);
3239
+ import_node_fs6.default.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
3240
+ import_node_fs6.default.renameSync(tmp, CACHE_FILE);
2446
3241
  } catch {
2447
3242
  }
2448
3243
  }
2449
3244
 
2450
3245
  // src/commands/update.ts
2451
- async function loadClients2() {
3246
+ async function loadClients3() {
2452
3247
  try {
2453
3248
  const mod = await Promise.resolve().then(() => (init_client_detector(), client_detector_exports));
2454
3249
  return mod.getInstalledClients();
@@ -2465,19 +3260,19 @@ function printTable(updates) {
2465
3260
  "LATEST".padEnd(VER_W),
2466
3261
  "STATUS"
2467
3262
  ].join(" ");
2468
- console.log(import_picocolors9.default.bold(`
3263
+ console.log(import_picocolors13.default.bold(`
2469
3264
  ${header}`));
2470
- console.log(import_picocolors9.default.dim(` ${"\u2500".repeat(NAME_W + VER_W * 2 + 20)}`));
3265
+ console.log(import_picocolors13.default.dim(` ${"\u2500".repeat(NAME_W + VER_W * 2 + 20)}`));
2471
3266
  for (const u of updates) {
2472
3267
  const nameCol = u.server.slice(0, NAME_W).padEnd(NAME_W);
2473
3268
  const curCol = u.currentVersion.padEnd(VER_W);
2474
3269
  const latCol = u.latestVersion.padEnd(VER_W);
2475
- const statusCol = u.hasUpdate ? import_picocolors9.default.yellow(`Update available${u.updateType ? ` [${u.updateType}]` : ""}`) : import_picocolors9.default.green("Up to date");
3270
+ const statusCol = u.hasUpdate ? import_picocolors13.default.yellow(`Update available${u.updateType ? ` [${u.updateType}]` : ""}`) : import_picocolors13.default.green("Up to date");
2476
3271
  console.log(` ${nameCol} ${curCol} ${latCol} ${statusCol}`);
2477
3272
  }
2478
3273
  console.log();
2479
3274
  }
2480
- var update_default = (0, import_citty9.defineCommand)({
3275
+ var update_default = (0, import_citty13.defineCommand)({
2481
3276
  meta: {
2482
3277
  name: "update",
2483
3278
  description: "Check for and apply updates to installed MCP servers"
@@ -2516,7 +3311,7 @@ var update_default = (0, import_citty9.defineCommand)({
2516
3311
  }
2517
3312
  process.exit(1);
2518
3313
  }
2519
- const spinner5 = p8.spinner();
3314
+ const spinner5 = p11.spinner();
2520
3315
  spinner5.start("Checking versions...");
2521
3316
  let updates;
2522
3317
  try {
@@ -2538,71 +3333,51 @@ var update_default = (0, import_citty9.defineCommand)({
2538
3333
  printTable(updates);
2539
3334
  const outdated = updates.filter((u) => u.hasUpdate);
2540
3335
  if (outdated.length === 0) {
2541
- console.log(import_picocolors9.default.green(" All servers are up to date."));
3336
+ console.log(import_picocolors13.default.green(" All servers are up to date."));
2542
3337
  return;
2543
3338
  }
2544
3339
  if (args.check) {
2545
- console.log(import_picocolors9.default.yellow(` ${outdated.length} update(s) available. Run mcpman update to apply.`));
3340
+ console.log(import_picocolors13.default.yellow(` ${outdated.length} update(s) available. Run mcpman update to apply.`));
2546
3341
  return;
2547
3342
  }
2548
3343
  if (!args.yes) {
2549
- const confirmed = await p8.confirm({
3344
+ const confirmed = await p11.confirm({
2550
3345
  message: `Apply ${outdated.length} update(s)?`,
2551
3346
  initialValue: true
2552
3347
  });
2553
- if (p8.isCancel(confirmed) || !confirmed) {
2554
- p8.outro("Cancelled.");
3348
+ if (p11.isCancel(confirmed) || !confirmed) {
3349
+ p11.outro("Cancelled.");
2555
3350
  return;
2556
3351
  }
2557
3352
  }
2558
- const clients = await loadClients2();
3353
+ const clients = await loadClients3();
2559
3354
  let successCount = 0;
2560
3355
  for (const update of outdated) {
2561
- const lockEntry = servers[update.server];
2562
- const input = lockEntry.source === "smithery" ? `smithery:${update.server}` : lockEntry.source === "github" ? lockEntry.resolved : update.server;
2563
- const s = p8.spinner();
3356
+ const s = p11.spinner();
2564
3357
  s.start(`Updating ${update.server}...`);
2565
- try {
2566
- const metadata = await resolveServer(input);
2567
- const integrity = computeIntegrity(metadata.resolved);
2568
- addEntry(update.server, {
2569
- ...lockEntry,
2570
- version: metadata.version,
2571
- resolved: metadata.resolved,
2572
- integrity,
2573
- command: metadata.command,
2574
- args: metadata.args,
2575
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
2576
- });
2577
- const entryClients = clients.filter(
2578
- (c) => lockEntry.clients.includes(c.type)
2579
- );
2580
- for (const client of entryClients) {
2581
- try {
2582
- await client.addServer(update.server, {
2583
- command: metadata.command,
2584
- args: metadata.args
2585
- });
2586
- } catch {
2587
- }
2588
- }
2589
- s.stop(`${import_picocolors9.default.green("\u2713")} ${update.server}: ${update.currentVersion} \u2192 ${metadata.version}`);
3358
+ const result = await applyServerUpdate(
3359
+ update.server,
3360
+ servers[update.server],
3361
+ clients
3362
+ );
3363
+ if (result.success) {
3364
+ s.stop(`${import_picocolors13.default.green("\u2713")} ${update.server}: ${result.fromVersion} \u2192 ${result.toVersion}`);
2590
3365
  successCount++;
2591
- } catch (err) {
2592
- s.stop(`${import_picocolors9.default.red("\u2717")} ${update.server}: ${err instanceof Error ? err.message : String(err)}`);
3366
+ } else {
3367
+ s.stop(`${import_picocolors13.default.red("\u2717")} ${update.server}: ${result.error}`);
2593
3368
  }
2594
3369
  }
2595
3370
  const freshLockfile = readLockfile(resolveLockfilePath());
2596
3371
  const freshUpdates = await checkAllVersions(freshLockfile);
2597
3372
  writeUpdateCache({ lastCheck: (/* @__PURE__ */ new Date()).toISOString(), updates: freshUpdates });
2598
- p8.outro(`${successCount} of ${outdated.length} server(s) updated.`);
3373
+ p11.outro(`${successCount} of ${outdated.length} server(s) updated.`);
2599
3374
  }
2600
3375
  });
2601
3376
 
2602
3377
  // src/utils/constants.ts
2603
3378
  init_cjs_shims();
2604
3379
  var APP_NAME = "mcpman";
2605
- var APP_VERSION = "0.2.0";
3380
+ var APP_VERSION = "0.4.0";
2606
3381
  var APP_DESCRIPTION = "The package manager for MCP servers";
2607
3382
 
2608
3383
  // src/index.ts
@@ -2610,7 +3385,7 @@ process.on("SIGINT", () => {
2610
3385
  console.log("\nAborted.");
2611
3386
  process.exit(130);
2612
3387
  });
2613
- var main = (0, import_citty10.defineCommand)({
3388
+ var main = (0, import_citty14.defineCommand)({
2614
3389
  meta: {
2615
3390
  name: APP_NAME,
2616
3391
  version: APP_VERSION,
@@ -2625,7 +3400,11 @@ var main = (0, import_citty10.defineCommand)({
2625
3400
  secrets: secrets_default,
2626
3401
  sync: sync_default,
2627
3402
  audit: audit_default,
2628
- update: update_default
3403
+ update: update_default,
3404
+ config: config_default,
3405
+ search: search_default,
3406
+ info: info_default,
3407
+ run: run_default
2629
3408
  }
2630
3409
  });
2631
- (0, import_citty10.runMain)(main);
3410
+ (0, import_citty14.runMain)(main);