mcpman 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # mcpman
2
2
 
3
- ![npm version](https://img.shields.io/npm/v/mcpman)
4
- ![license](https://img.shields.io/npm/l/mcpman)
3
+ [![npm version](https://img.shields.io/npm/v/mcpman)](https://www.npmjs.com/package/mcpman)
4
+ [![npm downloads](https://img.shields.io/npm/dm/mcpman)](https://www.npmjs.com/package/mcpman)
5
+ [![GitHub stars](https://img.shields.io/github/stars/tranhoangtu-it/mcpman)](https://github.com/tranhoangtu-it/mcpman)
6
+ [![license](https://img.shields.io/npm/l/mcpman)](https://github.com/tranhoangtu-it/mcpman/blob/main/LICENSE)
5
7
  ![node](https://img.shields.io/node/v/mcpman)
6
8
 
7
9
  **The package manager for MCP servers.**
@@ -33,6 +35,10 @@ mcpman install @modelcontextprotocol/server-filesystem
33
35
  - **Registry-aware** — resolves packages from npm, Smithery, or GitHub URLs
34
36
  - **Lockfile** — tracks installed servers in `mcpman.lock` for reproducible setups
35
37
  - **Health checks** — verifies runtimes, env vars, and server connectivity with `doctor`
38
+ - **Encrypted secrets** — store API keys in an AES-256 encrypted vault instead of plaintext JSON; auto-loads during install
39
+ - **Config sync** — keep server configs consistent across all your AI clients; `--remove` cleans extras
40
+ - **Security audit** — scan servers for vulnerabilities with trust scoring; `--fix` auto-updates vulnerable packages
41
+ - **Auto-update** — get notified when server updates are available
36
42
  - **Interactive prompts** — guided installation with env var configuration
37
43
  - **No extra daemon** — pure CLI, works anywhere Node ≥ 20 runs
38
44
 
@@ -92,6 +98,61 @@ Scaffold an `mcpman.lock` file in the current directory for project-scoped serve
92
98
  mcpman init
93
99
  ```
94
100
 
101
+ ### `secrets`
102
+
103
+ Manage encrypted secrets for MCP servers (API keys, tokens, etc.).
104
+
105
+ ```sh
106
+ mcpman secrets set my-server OPENAI_API_KEY=sk-...
107
+ mcpman secrets list my-server
108
+ mcpman secrets remove my-server OPENAI_API_KEY
109
+ ```
110
+
111
+ Secrets are stored in `~/.mcpman/vault.enc` using AES-256-CBC encryption with PBKDF2 key derivation. During `install`, vault secrets are auto-loaded to pre-fill env vars, and new credentials can be saved after installation.
112
+
113
+ ### `sync`
114
+
115
+ Sync MCP server configs across all detected AI clients.
116
+
117
+ ```sh
118
+ mcpman sync # sync all servers to all clients
119
+ mcpman sync --dry-run # preview changes without applying
120
+ mcpman sync --source cursor # use Cursor config as source of truth
121
+ mcpman sync --remove # remove servers not in lockfile from clients
122
+ ```
123
+
124
+ **Options:**
125
+ - `--dry-run` — preview changes without applying
126
+ - `--source <client>` — use a specific client config as source of truth
127
+ - `--remove` — remove extra servers from clients that aren't tracked in lockfile
128
+ - `--yes` — skip confirmation prompts
129
+
130
+ ### `audit [server]`
131
+
132
+ Scan installed servers for security vulnerabilities and compute trust scores.
133
+
134
+ ```sh
135
+ mcpman audit # audit all servers
136
+ mcpman audit my-server # audit specific server
137
+ mcpman audit --json # machine-readable output
138
+ mcpman audit --fix # auto-update vulnerable servers
139
+ mcpman audit --fix --yes # auto-update without confirmation
140
+ ```
141
+
142
+ Trust score (0–100) based on: vulnerability count, download velocity, package age, publish frequency, and maintainer signals.
143
+
144
+ The `--fix` flag checks for newer versions of vulnerable npm packages, updates them, and re-scans to verify the fixes.
145
+
146
+ ### `update [server]`
147
+
148
+ Check for and apply updates to installed MCP servers.
149
+
150
+ ```sh
151
+ mcpman update # update all servers
152
+ mcpman update my-server # update specific server
153
+ mcpman update --check # check only, don't apply
154
+ ```
155
+
95
156
  ---
96
157
 
97
158
  ## Comparison
@@ -101,6 +162,11 @@ mcpman init
101
162
  | Multi-client support | All 4 clients | Claude only | Limited |
102
163
  | Lockfile | `mcpman.lock` | None | None |
103
164
  | Health checks | Runtime + env + process | None | None |
165
+ | Encrypted secrets | AES-256 vault | None | None |
166
+ | Config sync | Cross-client + `--remove` | None | None |
167
+ | Security audit | Trust scoring + auto-fix | None | None |
168
+ | CI/CD | GitHub Actions | None | None |
169
+ | Auto-update | Version check + notify | None | None |
104
170
  | Registry sources | npm + Smithery + GitHub | Smithery only | npm only |
105
171
  | Interactive setup | Yes | Partial | No |
106
172
  | Project-scoped | Yes (`init`) | No | No |
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/core/vault-service.ts
4
+ import crypto from "crypto";
5
+ import fs from "fs";
6
+ import os from "os";
7
+ import path from "path";
8
+ import * as p from "@clack/prompts";
9
+ var _cachedPassword = null;
10
+ process.on("exit", () => {
11
+ _cachedPassword = null;
12
+ });
13
+ function getVaultPath() {
14
+ return path.join(os.homedir(), ".mcpman", "vault.enc");
15
+ }
16
+ function readVault(vaultPath = getVaultPath()) {
17
+ const empty = { version: 1, servers: {} };
18
+ try {
19
+ const raw = fs.readFileSync(vaultPath, "utf-8");
20
+ const parsed = JSON.parse(raw);
21
+ if (parsed.version !== 1 || typeof parsed.servers !== "object") return empty;
22
+ return parsed;
23
+ } catch {
24
+ return empty;
25
+ }
26
+ }
27
+ function writeVault(data, vaultPath = getVaultPath()) {
28
+ const dir = path.dirname(vaultPath);
29
+ fs.mkdirSync(dir, { recursive: true });
30
+ const tmp = `${vaultPath}.tmp`;
31
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
32
+ if (process.platform !== "win32") {
33
+ fs.chmodSync(tmp, 384);
34
+ }
35
+ fs.renameSync(tmp, vaultPath);
36
+ if (process.platform !== "win32") {
37
+ fs.chmodSync(vaultPath, 384);
38
+ }
39
+ }
40
+ function encrypt(value, password2) {
41
+ const salt = crypto.randomBytes(16);
42
+ const key = crypto.pbkdf2Sync(password2, salt, 1e5, 32, "sha256");
43
+ const iv = crypto.randomBytes(16);
44
+ const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
45
+ const encrypted = Buffer.concat([cipher.update(value, "utf-8"), cipher.final()]);
46
+ return {
47
+ salt: salt.toString("hex"),
48
+ iv: iv.toString("hex"),
49
+ data: encrypted.toString("hex")
50
+ };
51
+ }
52
+ function decrypt(entry, password2) {
53
+ const salt = Buffer.from(entry.salt, "hex");
54
+ const key = crypto.pbkdf2Sync(password2, salt, 1e5, 32, "sha256");
55
+ const iv = Buffer.from(entry.iv, "hex");
56
+ const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
57
+ const decrypted = Buffer.concat([
58
+ decipher.update(Buffer.from(entry.data, "hex")),
59
+ decipher.final()
60
+ // throws ERR_OSSL_BAD_DECRYPT on wrong password
61
+ ]);
62
+ return decrypted.toString("utf-8");
63
+ }
64
+ async function getMasterPassword(confirm = false) {
65
+ if (_cachedPassword) return _cachedPassword;
66
+ const password2 = await p.password({
67
+ message: "Enter vault master password:",
68
+ validate: (v) => v.length < 8 ? "Password must be at least 8 characters" : void 0
69
+ });
70
+ if (p.isCancel(password2)) {
71
+ p.cancel("Vault access cancelled.");
72
+ process.exit(0);
73
+ }
74
+ if (confirm) {
75
+ const confirm2 = await p.password({ message: "Confirm master password:" });
76
+ if (p.isCancel(confirm2) || confirm2 !== password2) {
77
+ p.cancel("Passwords do not match.");
78
+ process.exit(1);
79
+ }
80
+ }
81
+ _cachedPassword = password2;
82
+ return _cachedPassword;
83
+ }
84
+ function clearPasswordCache() {
85
+ _cachedPassword = null;
86
+ }
87
+ function setSecret(server, key, value, password2, vaultPath = getVaultPath()) {
88
+ const vault = readVault(vaultPath);
89
+ if (!vault.servers[server]) vault.servers[server] = {};
90
+ vault.servers[server][key] = encrypt(value, password2);
91
+ writeVault(vault, vaultPath);
92
+ }
93
+ function getSecret(server, key, password2, vaultPath = getVaultPath()) {
94
+ const vault = readVault(vaultPath);
95
+ const entry = vault.servers[server]?.[key];
96
+ if (!entry) return null;
97
+ return decrypt(entry, password2);
98
+ }
99
+ function getSecretsForServer(server, password2, vaultPath = getVaultPath()) {
100
+ const vault = readVault(vaultPath);
101
+ const entries = vault.servers[server];
102
+ if (!entries) return {};
103
+ const result = {};
104
+ for (const [k, entry] of Object.entries(entries)) {
105
+ result[k] = decrypt(entry, password2);
106
+ }
107
+ return result;
108
+ }
109
+ function removeSecret(server, key, vaultPath = getVaultPath()) {
110
+ const vault = readVault(vaultPath);
111
+ if (vault.servers[server]) {
112
+ delete vault.servers[server][key];
113
+ if (Object.keys(vault.servers[server]).length === 0) {
114
+ delete vault.servers[server];
115
+ }
116
+ writeVault(vault, vaultPath);
117
+ }
118
+ }
119
+ function listSecrets(server, vaultPath = getVaultPath()) {
120
+ const vault = readVault(vaultPath);
121
+ const entries = server ? vault.servers[server] ? { [server]: vault.servers[server] } : {} : vault.servers;
122
+ return Object.entries(entries).map(([srv, keys]) => ({
123
+ server: srv,
124
+ keys: Object.keys(keys)
125
+ }));
126
+ }
127
+
128
+ export {
129
+ getVaultPath,
130
+ readVault,
131
+ writeVault,
132
+ encrypt,
133
+ decrypt,
134
+ getMasterPassword,
135
+ clearPasswordCache,
136
+ setSecret,
137
+ getSecret,
138
+ getSecretsForServer,
139
+ removeSecret,
140
+ listSecrets
141
+ };