mcpman 0.1.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 +119 -0
- package/dist/chunk-QY22QTBR.js +229 -0
- package/dist/client-detector-SUIJSIYM.js +11 -0
- package/dist/index.cjs +1407 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1059 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# mcpman
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
**The package manager for MCP servers.**
|
|
8
|
+
|
|
9
|
+
Install, manage, and inspect Model Context Protocol servers across all your AI clients — Claude Desktop, Cursor, VS Code, and Windsurf — from a single CLI.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
# Install an MCP server globally (no install required)
|
|
17
|
+
npx mcpman install @modelcontextprotocol/server-filesystem
|
|
18
|
+
|
|
19
|
+
# Or install mcpman globally
|
|
20
|
+
npm install -g mcpman
|
|
21
|
+
mcpman install @modelcontextprotocol/server-filesystem
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Universal** — manages servers for Claude Desktop, Cursor, VS Code, and Windsurf from one tool
|
|
29
|
+
- **Registry-aware** — resolves packages from npm, Smithery, or GitHub URLs
|
|
30
|
+
- **Lockfile** — tracks installed servers in `mcpman.lock` for reproducible setups
|
|
31
|
+
- **Health checks** — verifies runtimes, env vars, and server connectivity with `doctor`
|
|
32
|
+
- **Interactive prompts** — guided installation with env var configuration
|
|
33
|
+
- **No extra daemon** — pure CLI, works anywhere Node ≥ 20 runs
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
### `install <server>`
|
|
40
|
+
|
|
41
|
+
Install an MCP server and register it with your AI clients.
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
mcpman install @modelcontextprotocol/server-filesystem
|
|
45
|
+
mcpman install my-smithery-server
|
|
46
|
+
mcpman install https://github.com/owner/repo
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Options:**
|
|
50
|
+
- `--client <type>` — target a specific client (`claude-desktop`, `cursor`, `vscode`, `windsurf`)
|
|
51
|
+
- `--json` — output machine-readable JSON
|
|
52
|
+
|
|
53
|
+
### `list`
|
|
54
|
+
|
|
55
|
+
List all installed MCP servers.
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
mcpman list
|
|
59
|
+
mcpman list --json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Shows server name, version, runtime, source, and which clients have it registered.
|
|
63
|
+
|
|
64
|
+
### `remove <server>`
|
|
65
|
+
|
|
66
|
+
Uninstall a server and deregister it from all clients.
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
mcpman remove @modelcontextprotocol/server-filesystem
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `doctor [server]`
|
|
73
|
+
|
|
74
|
+
Run health diagnostics on all installed servers or a specific one.
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
mcpman doctor
|
|
78
|
+
mcpman doctor my-server
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Checks: runtime availability, required env vars, process spawn, and MCP handshake.
|
|
82
|
+
|
|
83
|
+
### `init`
|
|
84
|
+
|
|
85
|
+
Scaffold an `mcpman.lock` file in the current directory for project-scoped server management.
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
mcpman init
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Comparison
|
|
94
|
+
|
|
95
|
+
| Feature | mcpman | Smithery CLI | mcpm.sh |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| Multi-client support | All 4 clients | Claude only | Limited |
|
|
98
|
+
| Lockfile | `mcpman.lock` | None | None |
|
|
99
|
+
| Health checks | Runtime + env + process | None | None |
|
|
100
|
+
| Registry sources | npm + Smithery + GitHub | Smithery only | npm only |
|
|
101
|
+
| Interactive setup | Yes | Partial | No |
|
|
102
|
+
| Project-scoped | Yes (`init`) | No | No |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Contributing
|
|
107
|
+
|
|
108
|
+
1. Fork the repo and create a feature branch
|
|
109
|
+
2. `npm install` to install dependencies
|
|
110
|
+
3. `npm test` to run the test suite
|
|
111
|
+
4. Submit a pull request with a clear description
|
|
112
|
+
|
|
113
|
+
Please follow the existing code style (TypeScript strict, ES modules).
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/clients/base-client-handler.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
// src/clients/types.ts
|
|
8
|
+
var ConfigParseError = class extends Error {
|
|
9
|
+
constructor(configPath, cause) {
|
|
10
|
+
super(`Failed to parse config: ${configPath} \u2014 ${String(cause)}`);
|
|
11
|
+
this.configPath = configPath;
|
|
12
|
+
this.name = "ConfigParseError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var ConfigWriteError = class extends Error {
|
|
16
|
+
constructor(configPath, cause) {
|
|
17
|
+
super(`Failed to write config: ${configPath} \u2014 ${String(cause)}`);
|
|
18
|
+
this.configPath = configPath;
|
|
19
|
+
this.name = "ConfigWriteError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/clients/base-client-handler.ts
|
|
24
|
+
async function atomicWrite(filePath, content) {
|
|
25
|
+
const tmpPath = `${filePath}.tmp`;
|
|
26
|
+
try {
|
|
27
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
28
|
+
await fs.promises.writeFile(tmpPath, content, { encoding: "utf-8", mode: 384 });
|
|
29
|
+
await fs.promises.rename(tmpPath, filePath);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
try {
|
|
32
|
+
await fs.promises.unlink(tmpPath);
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function pathExists(p) {
|
|
39
|
+
try {
|
|
40
|
+
await fs.promises.access(p);
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
var BaseClientHandler = class {
|
|
47
|
+
async isInstalled() {
|
|
48
|
+
const dir = path.dirname(this.getConfigPath());
|
|
49
|
+
return pathExists(dir);
|
|
50
|
+
}
|
|
51
|
+
/** Read raw JSON from disk, return empty object if file missing */
|
|
52
|
+
async readRaw() {
|
|
53
|
+
const configPath = this.getConfigPath();
|
|
54
|
+
try {
|
|
55
|
+
const raw = await fs.promises.readFile(configPath, "utf-8");
|
|
56
|
+
return JSON.parse(raw);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err.code === "ENOENT") {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
throw new ConfigParseError(configPath, err);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Serialize raw object to disk atomically */
|
|
65
|
+
async writeRaw(data) {
|
|
66
|
+
const configPath = this.getConfigPath();
|
|
67
|
+
try {
|
|
68
|
+
await atomicWrite(configPath, JSON.stringify(data, null, 2));
|
|
69
|
+
} catch (err) {
|
|
70
|
+
throw new ConfigWriteError(configPath, err);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Convert raw JSON to ClientConfig — override for non-standard formats */
|
|
74
|
+
toClientConfig(raw) {
|
|
75
|
+
const mcpServers = raw.mcpServers ?? {};
|
|
76
|
+
return { servers: mcpServers };
|
|
77
|
+
}
|
|
78
|
+
/** Merge ClientConfig back into raw JSON — override for non-standard formats */
|
|
79
|
+
fromClientConfig(raw, config) {
|
|
80
|
+
return { ...raw, mcpServers: config.servers };
|
|
81
|
+
}
|
|
82
|
+
async readConfig() {
|
|
83
|
+
const raw = await this.readRaw();
|
|
84
|
+
return this.toClientConfig(raw);
|
|
85
|
+
}
|
|
86
|
+
async writeConfig(config) {
|
|
87
|
+
const raw = await this.readRaw();
|
|
88
|
+
await this.writeRaw(this.fromClientConfig(raw, config));
|
|
89
|
+
}
|
|
90
|
+
async addServer(name, entry) {
|
|
91
|
+
const config = await this.readConfig();
|
|
92
|
+
config.servers[name] = entry;
|
|
93
|
+
await this.writeConfig(config);
|
|
94
|
+
}
|
|
95
|
+
async removeServer(name) {
|
|
96
|
+
const config = await this.readConfig();
|
|
97
|
+
delete config.servers[name];
|
|
98
|
+
await this.writeConfig(config);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
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 getAppDataDir() {
|
|
109
|
+
const home = getHomedir();
|
|
110
|
+
if (process.platform === "darwin") {
|
|
111
|
+
return path2.join(home, "Library", "Application Support");
|
|
112
|
+
}
|
|
113
|
+
if (process.platform === "win32") {
|
|
114
|
+
return process.env.APPDATA ?? path2.join(home, "AppData", "Roaming");
|
|
115
|
+
}
|
|
116
|
+
return process.env.XDG_CONFIG_HOME ?? path2.join(home, ".config");
|
|
117
|
+
}
|
|
118
|
+
function resolveConfigPath(client) {
|
|
119
|
+
const appData = getAppDataDir();
|
|
120
|
+
const home = getHomedir();
|
|
121
|
+
switch (client) {
|
|
122
|
+
case "claude-desktop":
|
|
123
|
+
return path2.join(appData, "Claude", "claude_desktop_config.json");
|
|
124
|
+
case "cursor":
|
|
125
|
+
return path2.join(
|
|
126
|
+
appData,
|
|
127
|
+
"Cursor",
|
|
128
|
+
"User",
|
|
129
|
+
"globalStorage",
|
|
130
|
+
"cursor.mcp",
|
|
131
|
+
"mcp.json"
|
|
132
|
+
);
|
|
133
|
+
case "windsurf":
|
|
134
|
+
return path2.join(
|
|
135
|
+
appData,
|
|
136
|
+
"Windsurf",
|
|
137
|
+
"User",
|
|
138
|
+
"globalStorage",
|
|
139
|
+
"windsurf.mcpConfigJson",
|
|
140
|
+
"mcp.json"
|
|
141
|
+
);
|
|
142
|
+
case "vscode":
|
|
143
|
+
if (process.platform === "darwin") {
|
|
144
|
+
return path2.join(appData, "Code", "User", "settings.json");
|
|
145
|
+
}
|
|
146
|
+
if (process.platform === "win32") {
|
|
147
|
+
return path2.join(appData, "Code", "User", "settings.json");
|
|
148
|
+
}
|
|
149
|
+
return path2.join(home, ".config", "Code", "User", "settings.json");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/clients/claude-desktop.ts
|
|
154
|
+
var ClaudeDesktopHandler = class extends BaseClientHandler {
|
|
155
|
+
type = "claude-desktop";
|
|
156
|
+
displayName = "Claude Desktop";
|
|
157
|
+
getConfigPath() {
|
|
158
|
+
return resolveConfigPath("claude-desktop");
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// src/clients/cursor.ts
|
|
163
|
+
var CursorHandler = class extends BaseClientHandler {
|
|
164
|
+
type = "cursor";
|
|
165
|
+
displayName = "Cursor";
|
|
166
|
+
getConfigPath() {
|
|
167
|
+
return resolveConfigPath("cursor");
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// src/clients/vscode.ts
|
|
172
|
+
var VSCodeHandler = class extends BaseClientHandler {
|
|
173
|
+
type = "vscode";
|
|
174
|
+
displayName = "VS Code";
|
|
175
|
+
getConfigPath() {
|
|
176
|
+
return resolveConfigPath("vscode");
|
|
177
|
+
}
|
|
178
|
+
toClientConfig(raw) {
|
|
179
|
+
const mcp = raw.mcp ?? {};
|
|
180
|
+
const servers = mcp.servers ?? {};
|
|
181
|
+
return { servers };
|
|
182
|
+
}
|
|
183
|
+
fromClientConfig(raw, config) {
|
|
184
|
+
const existingMcp = raw.mcp ?? {};
|
|
185
|
+
return {
|
|
186
|
+
...raw,
|
|
187
|
+
mcp: { ...existingMcp, servers: config.servers }
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// src/clients/windsurf.ts
|
|
193
|
+
var WindsurfHandler = class extends BaseClientHandler {
|
|
194
|
+
type = "windsurf";
|
|
195
|
+
displayName = "Windsurf";
|
|
196
|
+
getConfigPath() {
|
|
197
|
+
return resolveConfigPath("windsurf");
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// src/clients/client-detector.ts
|
|
202
|
+
function getAllClientTypes() {
|
|
203
|
+
return ["claude-desktop", "cursor", "vscode", "windsurf"];
|
|
204
|
+
}
|
|
205
|
+
function getClient(type) {
|
|
206
|
+
switch (type) {
|
|
207
|
+
case "claude-desktop":
|
|
208
|
+
return new ClaudeDesktopHandler();
|
|
209
|
+
case "cursor":
|
|
210
|
+
return new CursorHandler();
|
|
211
|
+
case "vscode":
|
|
212
|
+
return new VSCodeHandler();
|
|
213
|
+
case "windsurf":
|
|
214
|
+
return new WindsurfHandler();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function getInstalledClients() {
|
|
218
|
+
const all = getAllClientTypes().map(getClient);
|
|
219
|
+
const results = await Promise.all(
|
|
220
|
+
all.map(async (handler) => ({ handler, installed: await handler.isInstalled() }))
|
|
221
|
+
);
|
|
222
|
+
return results.filter((r) => r.installed).map((r) => r.handler);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export {
|
|
226
|
+
getAllClientTypes,
|
|
227
|
+
getClient,
|
|
228
|
+
getInstalledClients
|
|
229
|
+
};
|