mcpman 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -2
- package/dist/chunk-6X6Q6UZC.js +141 -0
- package/dist/index.cjs +1769 -356
- package/dist/index.js +1479 -308
- package/dist/trust-scorer-LYC6KZCD.js +77 -0
- package/dist/vault-service-UTZAV6N6.js +29 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# mcpman
|
|
2
2
|
|
|
3
|
-

|
|
4
|
-
](https://www.npmjs.com/package/mcpman)
|
|
4
|
+
[](https://www.npmjs.com/package/mcpman)
|
|
5
|
+
[](https://github.com/tranhoangtu-it/mcpman)
|
|
6
|
+
[](https://github.com/tranhoangtu-it/mcpman/blob/main/LICENSE)
|
|
5
7
|

|
|
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
|
+
};
|