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 ADDED
@@ -0,0 +1,119 @@
1
+ # mcpman
2
+
3
+ ![npm version](https://img.shields.io/npm/v/mcpman)
4
+ ![license](https://img.shields.io/npm/l/mcpman)
5
+ ![node](https://img.shields.io/node/v/mcpman)
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
+ };
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getAllClientTypes,
4
+ getClient,
5
+ getInstalledClients
6
+ } from "./chunk-QY22QTBR.js";
7
+ export {
8
+ getAllClientTypes,
9
+ getClient,
10
+ getInstalledClients
11
+ };