mcpman 0.4.0 → 0.7.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
@@ -39,6 +39,12 @@ mcpman install @modelcontextprotocol/server-filesystem
39
39
  - **Config sync** — keep server configs consistent across all your AI clients; `--remove` cleans extras
40
40
  - **Security audit** — scan servers for vulnerabilities with trust scoring; `--fix` auto-updates vulnerable packages
41
41
  - **Auto-update** — get notified when server updates are available
42
+ - **Plugin system** — extend mcpman with npm-based plugins for custom registries (e.g. Ollama, HuggingFace)
43
+ - **Export/Import** — portable JSON bundles for full config migration across machines
44
+ - **Server testing** — validate MCP servers respond to JSON-RPC initialize + tools/list
45
+ - **Log streaming** — stream stdout/stderr from servers in real time
46
+ - **Profiles** — save/restore named server configurations for quick switching
47
+ - **Self-upgrade** — update mcpman itself with a single command
42
48
  - **Interactive prompts** — guided installation with env var configuration
43
49
  - **No extra daemon** — pure CLI, works anywhere Node ≥ 20 runs
44
50
 
@@ -153,6 +159,207 @@ mcpman update my-server # update specific server
153
159
  mcpman update --check # check only, don't apply
154
160
  ```
155
161
 
162
+ ### `config <set|get|list|reset>`
163
+
164
+ Manage persistent CLI configuration at `~/.mcpman/config.json`.
165
+
166
+ ```sh
167
+ mcpman config set defaultClient cursor
168
+ mcpman config get defaultClient
169
+ mcpman config list
170
+ mcpman config reset
171
+ ```
172
+
173
+ Keys: `defaultClient`, `updateCheckInterval`, `preferredRegistry`, `vaultTimeout`, `plugins`.
174
+
175
+ ### `search <query>`
176
+
177
+ Search for MCP servers on npm or Smithery registry.
178
+
179
+ ```sh
180
+ mcpman search filesystem
181
+ mcpman search brave --registry smithery
182
+ mcpman search tools --all # include plugin registries
183
+ mcpman search tools --limit 10
184
+ ```
185
+
186
+ **Options:**
187
+ - `--registry <npm|smithery>` — registry to search (default: npm)
188
+ - `--limit <n>` — max results (default: 20, max: 100)
189
+ - `--all` — include plugin registries in results
190
+
191
+ ### `info <server>`
192
+
193
+ Show detailed information about an MCP server package.
194
+
195
+ ```sh
196
+ mcpman info @modelcontextprotocol/server-filesystem
197
+ mcpman info my-server --json
198
+ ```
199
+
200
+ ### `run <server>`
201
+
202
+ Launch an MCP server with vault secrets auto-injected into the process environment.
203
+
204
+ ```sh
205
+ mcpman run my-server
206
+ mcpman run my-server --env API_KEY=sk-...
207
+ ```
208
+
209
+ ### `upgrade`
210
+
211
+ Upgrade mcpman itself to the latest version from npm.
212
+
213
+ ```sh
214
+ mcpman upgrade # check and install latest
215
+ mcpman upgrade --check # only check, don't install
216
+ ```
217
+
218
+ ### `test [server]`
219
+
220
+ Validate MCP server connectivity by sending JSON-RPC `initialize` + `tools/list`.
221
+
222
+ ```sh
223
+ mcpman test my-server # test a specific server
224
+ mcpman test --all # test all installed servers
225
+ ```
226
+
227
+ Reports pass/fail, response time, and discovered tools for each server.
228
+
229
+ ### `logs <server>`
230
+
231
+ Stream stdout/stderr from an MCP server process in real time.
232
+
233
+ ```sh
234
+ mcpman logs my-server # stream logs (Ctrl+C to stop)
235
+ ```
236
+
237
+ Vault secrets are auto-injected into the server environment.
238
+
239
+ ### `profiles <create|switch|list|delete>`
240
+
241
+ Manage named server configuration profiles for quick switching.
242
+
243
+ ```sh
244
+ mcpman profiles create dev # snapshot current servers as "dev"
245
+ mcpman profiles create prod -d "Production config"
246
+ mcpman profiles list # show all profiles
247
+ mcpman profiles switch dev # apply "dev" profile to lockfile
248
+ mcpman profiles delete old # remove a profile
249
+ ```
250
+
251
+ After switching, run `mcpman sync` to apply the profile to all clients.
252
+
253
+ ### `plugin <add|remove|list>`
254
+
255
+ Manage mcpman plugins for custom registries.
256
+
257
+ ```sh
258
+ mcpman plugin add mcpman-plugin-ollama # install plugin
259
+ mcpman plugin remove mcpman-plugin-ollama # uninstall plugin
260
+ mcpman plugin list # show installed plugins
261
+ ```
262
+
263
+ Plugins are npm packages that export a `McpmanPlugin` interface with `name`, `prefix`, and `resolve()`. Once installed, their prefix (e.g. `ollama:`) works with `mcpman install ollama:my-model`.
264
+
265
+ ### `export [output-file]`
266
+
267
+ Export mcpman config, lockfile, vault, and plugins to a portable JSON file.
268
+
269
+ ```sh
270
+ mcpman export # default: mcpman-export.json
271
+ mcpman export backup.json
272
+ mcpman export --no-vault # exclude encrypted vault
273
+ mcpman export --no-plugins # exclude plugin list
274
+ ```
275
+
276
+ ### `import <file>`
277
+
278
+ Restore mcpman config, lockfile, vault, and plugins from an export bundle.
279
+
280
+ ```sh
281
+ mcpman import mcpman-export.json
282
+ mcpman import backup.json --yes # skip confirmation
283
+ mcpman import backup.json --dry-run # preview without applying
284
+ ```
285
+
286
+ ### `create [name]`
287
+
288
+ Scaffold a new MCP server project with working boilerplate.
289
+
290
+ ```sh
291
+ mcpman create my-server # interactive prompts
292
+ mcpman create my-server --yes # accept defaults
293
+ mcpman create my-server --runtime python # Python template
294
+ ```
295
+
296
+ Generates `package.json` (with `mcp` field), `src/index.ts`, and `tsconfig.json` for Node; or `pyproject.toml` and `main.py` for Python. Both templates implement the MCP protocol with a sample `hello` tool ready to run.
297
+
298
+ ### `link [dir]`
299
+
300
+ Register a local MCP server directory with AI clients — like `npm link` but for MCP.
301
+
302
+ ```sh
303
+ mcpman link . # link current directory
304
+ mcpman link ./path/to/server # link specific directory
305
+ mcpman link . --client cursor # link to specific client only
306
+ mcpman link . --name my-override # override detected server name
307
+ ```
308
+
309
+ Reads `package.json` or `pyproject.toml` to detect name, version, and entry point. Adds a lockfile entry with `source: "local"` and registers the absolute path in client configs. No file copying — edits are picked up immediately.
310
+
311
+ ### `watch <server>`
312
+
313
+ Watch a local MCP server's source files and auto-restart on changes — like nodemon, built into mcpman.
314
+
315
+ ```sh
316
+ mcpman watch my-server # watch with defaults
317
+ mcpman watch my-server --dir ./src # override watch directory
318
+ mcpman watch my-server --ext ts,js # watch specific extensions
319
+ mcpman watch my-server --delay 500 # set debounce delay (ms)
320
+ mcpman watch my-server --clear # clear terminal on restart
321
+ ```
322
+
323
+ Uses Node.js built-in `fs.watch` (no chokidar). Debounces 300ms by default. Ignores `node_modules/`, `dist/`, `.git/`, `__pycache__/`. Vault secrets are injected same as `mcpman run`.
324
+
325
+ ### `registry <list|add|remove|set-default>`
326
+
327
+ Manage custom registry URLs for MCP server resolution.
328
+
329
+ ```sh
330
+ mcpman registry list # show all registries
331
+ mcpman registry add corp https://mcp.corp.com/api # add custom registry
332
+ mcpman registry remove corp # remove custom registry
333
+ mcpman registry set-default smithery # change default registry
334
+ ```
335
+
336
+ Built-in registries (npm, smithery) are always present and cannot be removed. Custom registries are stored in `~/.mcpman/config.json`.
337
+
338
+ ### `completions <bash|zsh|fish|install>`
339
+
340
+ Generate shell completion scripts for tab-completion of commands and server names.
341
+
342
+ ```sh
343
+ mcpman completions bash # output bash completion script
344
+ mcpman completions zsh # output zsh completion script
345
+ mcpman completions fish # output fish completion script
346
+ mcpman completions install # auto-detect shell and install
347
+ source <(mcpman completions bash) # enable completions in current session
348
+ ```
349
+
350
+ Completes: subcommands, server names (from lockfile), client types (`--client`), and runtimes (`--runtime`). Server names are resolved dynamically at completion time so they stay fresh.
351
+
352
+ ### `why <server>`
353
+
354
+ Show why a server is installed — source, clients, profiles, env vars.
355
+
356
+ ```sh
357
+ mcpman why my-server # full provenance output
358
+ mcpman why my-server --json # JSON output for scripting
359
+ ```
360
+
361
+ Displays: source (npm/smithery/github/local), resolved URL, version, installed timestamp, which clients have it registered, which named profiles include it, and required env var names. Detects orphaned servers (in client config but not in lockfile) and suggests `mcpman sync --remove`.
362
+
156
363
  ---
157
364
 
158
365
  ## Comparison
@@ -168,8 +375,20 @@ mcpman update --check # check only, don't apply
168
375
  | CI/CD | GitHub Actions | None | None |
169
376
  | Auto-update | Version check + notify | None | None |
170
377
  | Registry sources | npm + Smithery + GitHub | Smithery only | npm only |
378
+ | Plugin system | npm-based custom registries | None | None |
379
+ | Export/Import | Full config portability | None | None |
380
+ | Server testing | JSON-RPC validation | None | None |
381
+ | Log streaming | Real-time stdout/stderr | None | None |
382
+ | Profiles | Named config switching | None | None |
383
+ | Self-upgrade | Built-in CLI updater | None | None |
171
384
  | Interactive setup | Yes | Partial | No |
172
385
  | Project-scoped | Yes (`init`) | No | No |
386
+ | Server scaffolding | `create` (Node + Python) | None | None |
387
+ | Local dev linking | `link` (like npm link) | None | None |
388
+ | File watching | `watch` (auto-restart) | None | None |
389
+ | Custom registries | `registry` CRUD | None | None |
390
+ | Shell completions | bash + zsh + fish | None | None |
391
+ | Provenance query | `why` (clients + profiles) | None | None |
173
392
 
174
393
  ---
175
394
 
@@ -1,8 +1,64 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/utils/paths.ts
4
+ import os from "os";
5
+ import path from "path";
6
+ function getHomedir() {
7
+ return os.homedir();
8
+ }
9
+ function getMcpmanDir() {
10
+ return path.join(os.homedir(), ".mcpman");
11
+ }
12
+ function getConfigPath() {
13
+ return path.join(getMcpmanDir(), "config.json");
14
+ }
15
+ function getPluginDir() {
16
+ return path.join(getMcpmanDir(), "plugins");
17
+ }
18
+ function getProfilesDir() {
19
+ return path.join(getMcpmanDir(), "profiles");
20
+ }
21
+ function getAppDataDir() {
22
+ const home = getHomedir();
23
+ if (process.platform === "darwin") {
24
+ return path.join(home, "Library", "Application Support");
25
+ }
26
+ if (process.platform === "win32") {
27
+ return process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
28
+ }
29
+ return process.env.XDG_CONFIG_HOME ?? path.join(home, ".config");
30
+ }
31
+ function resolveConfigPath(client) {
32
+ const appData = getAppDataDir();
33
+ const home = getHomedir();
34
+ switch (client) {
35
+ case "claude-desktop":
36
+ return path.join(appData, "Claude", "claude_desktop_config.json");
37
+ case "cursor":
38
+ return path.join(appData, "Cursor", "User", "globalStorage", "cursor.mcp", "mcp.json");
39
+ case "windsurf":
40
+ return path.join(
41
+ appData,
42
+ "Windsurf",
43
+ "User",
44
+ "globalStorage",
45
+ "windsurf.mcpConfigJson",
46
+ "mcp.json"
47
+ );
48
+ case "vscode":
49
+ if (process.platform === "darwin") {
50
+ return path.join(appData, "Code", "User", "settings.json");
51
+ }
52
+ if (process.platform === "win32") {
53
+ return path.join(appData, "Code", "User", "settings.json");
54
+ }
55
+ return path.join(home, ".config", "Code", "User", "settings.json");
56
+ }
57
+ }
58
+
3
59
  // src/clients/base-client-handler.ts
4
60
  import fs from "fs";
5
- import path from "path";
61
+ import path2 from "path";
6
62
 
7
63
  // src/clients/types.ts
8
64
  var ConfigParseError = class extends Error {
@@ -24,7 +80,7 @@ var ConfigWriteError = class extends Error {
24
80
  async function atomicWrite(filePath, content) {
25
81
  const tmpPath = `${filePath}.tmp`;
26
82
  try {
27
- await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
83
+ await fs.promises.mkdir(path2.dirname(filePath), { recursive: true });
28
84
  await fs.promises.writeFile(tmpPath, content, { encoding: "utf-8", mode: 384 });
29
85
  await fs.promises.rename(tmpPath, filePath);
30
86
  } catch (err) {
@@ -45,7 +101,7 @@ async function pathExists(p) {
45
101
  }
46
102
  var BaseClientHandler = class {
47
103
  async isInstalled() {
48
- const dir = path.dirname(this.getConfigPath());
104
+ const dir = path2.dirname(this.getConfigPath());
49
105
  return pathExists(dir);
50
106
  }
51
107
  /** Read raw JSON from disk, return empty object if file missing */
@@ -99,63 +155,6 @@ var BaseClientHandler = class {
99
155
  }
100
156
  };
101
157
 
102
- // src/utils/paths.ts
103
- import os from "os";
104
- import path2 from "path";
105
- function getHomedir() {
106
- return os.homedir();
107
- }
108
- function getMcpmanDir() {
109
- return path2.join(os.homedir(), ".mcpman");
110
- }
111
- function getConfigPath() {
112
- return path2.join(getMcpmanDir(), "config.json");
113
- }
114
- function getAppDataDir() {
115
- const home = getHomedir();
116
- if (process.platform === "darwin") {
117
- return path2.join(home, "Library", "Application Support");
118
- }
119
- if (process.platform === "win32") {
120
- return process.env.APPDATA ?? path2.join(home, "AppData", "Roaming");
121
- }
122
- return process.env.XDG_CONFIG_HOME ?? path2.join(home, ".config");
123
- }
124
- function resolveConfigPath(client) {
125
- const appData = getAppDataDir();
126
- const home = getHomedir();
127
- switch (client) {
128
- case "claude-desktop":
129
- return path2.join(appData, "Claude", "claude_desktop_config.json");
130
- case "cursor":
131
- return path2.join(
132
- appData,
133
- "Cursor",
134
- "User",
135
- "globalStorage",
136
- "cursor.mcp",
137
- "mcp.json"
138
- );
139
- case "windsurf":
140
- return path2.join(
141
- appData,
142
- "Windsurf",
143
- "User",
144
- "globalStorage",
145
- "windsurf.mcpConfigJson",
146
- "mcp.json"
147
- );
148
- case "vscode":
149
- if (process.platform === "darwin") {
150
- return path2.join(appData, "Code", "User", "settings.json");
151
- }
152
- if (process.platform === "win32") {
153
- return path2.join(appData, "Code", "User", "settings.json");
154
- }
155
- return path2.join(home, ".config", "Code", "User", "settings.json");
156
- }
157
- }
158
-
159
158
  // src/clients/claude-desktop.ts
160
159
  var ClaudeDesktopHandler = class extends BaseClientHandler {
161
160
  type = "claude-desktop";
@@ -230,6 +229,8 @@ async function getInstalledClients() {
230
229
 
231
230
  export {
232
231
  getConfigPath,
232
+ getPluginDir,
233
+ getProfilesDir,
233
234
  getAllClientTypes,
234
235
  getClient,
235
236
  getInstalledClients
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/core/lockfile.ts
4
+ import fs from "fs";
5
+ import os from "os";
6
+ import path from "path";
7
+ var LOCKFILE_NAME = "mcpman.lock";
8
+ function findLockfile() {
9
+ let dir = process.cwd();
10
+ while (true) {
11
+ const candidate = path.join(dir, LOCKFILE_NAME);
12
+ if (fs.existsSync(candidate)) return candidate;
13
+ const parent = path.dirname(dir);
14
+ if (parent === dir) break;
15
+ dir = parent;
16
+ }
17
+ return null;
18
+ }
19
+ function getGlobalLockfilePath() {
20
+ return path.join(os.homedir(), ".mcpman", LOCKFILE_NAME);
21
+ }
22
+ function resolveLockfilePath() {
23
+ return findLockfile() ?? getGlobalLockfilePath();
24
+ }
25
+ function readLockfile(filePath) {
26
+ const target = filePath ?? resolveLockfilePath();
27
+ if (!fs.existsSync(target)) {
28
+ return { lockfileVersion: 1, servers: {} };
29
+ }
30
+ try {
31
+ const raw = fs.readFileSync(target, "utf-8");
32
+ return JSON.parse(raw);
33
+ } catch {
34
+ return { lockfileVersion: 1, servers: {} };
35
+ }
36
+ }
37
+ function serialize(data) {
38
+ const sorted = {
39
+ lockfileVersion: data.lockfileVersion,
40
+ servers: Object.fromEntries(
41
+ Object.entries(data.servers).sort(([a], [b]) => a.localeCompare(b))
42
+ )
43
+ };
44
+ return `${JSON.stringify(sorted, null, 2)}
45
+ `;
46
+ }
47
+ function writeLockfile(data, filePath) {
48
+ const target = filePath ?? resolveLockfilePath();
49
+ const dir = path.dirname(target);
50
+ if (!fs.existsSync(dir)) {
51
+ fs.mkdirSync(dir, { recursive: true });
52
+ }
53
+ const tmp = `${target}.tmp`;
54
+ fs.writeFileSync(tmp, serialize(data), "utf-8");
55
+ fs.renameSync(tmp, target);
56
+ }
57
+ function addEntry(name, entry, filePath) {
58
+ const data = readLockfile(filePath);
59
+ data.servers[name] = entry;
60
+ writeLockfile(data, filePath);
61
+ }
62
+ function removeEntry(name, filePath) {
63
+ const data = readLockfile(filePath);
64
+ if (name in data.servers) {
65
+ delete data.servers[name];
66
+ writeLockfile(data, filePath);
67
+ }
68
+ }
69
+ function getLockedVersion(name, filePath) {
70
+ const data = readLockfile(filePath);
71
+ return data.servers[name]?.version;
72
+ }
73
+ function createEmptyLockfile(filePath) {
74
+ writeLockfile({ lockfileVersion: 1, servers: {} }, filePath);
75
+ }
76
+
77
+ export {
78
+ LOCKFILE_NAME,
79
+ findLockfile,
80
+ getGlobalLockfilePath,
81
+ resolveLockfilePath,
82
+ readLockfile,
83
+ writeLockfile,
84
+ addEntry,
85
+ removeEntry,
86
+ getLockedVersion,
87
+ createEmptyLockfile
88
+ };
@@ -3,7 +3,7 @@ import {
3
3
  getAllClientTypes,
4
4
  getClient,
5
5
  getInstalledClients
6
- } from "./chunk-RMMEBP2J.js";
6
+ } from "./chunk-NS6HV723.js";
7
7
  export {
8
8
  getAllClientTypes,
9
9
  getClient,