mcp-config-manager 1.0.2 → 1.0.4
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 +24 -3
- package/package.json +2 -1
- package/src/cli.js +101 -5
- package/src/config-manager.js +52 -17
package/README.md
CHANGED
|
@@ -101,11 +101,32 @@ npm run web
|
|
|
101
101
|
|
|
102
102
|
### Web UI
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
#### Development (when cloning repository)
|
|
105
105
|
```bash
|
|
106
106
|
npm run web
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Global Installation
|
|
110
|
+
After installing globally with `npm install -g mcp-config-manager`:
|
|
111
|
+
|
|
112
|
+
**Start web server in foreground:**
|
|
113
|
+
```bash
|
|
114
|
+
mcp-config-manager web --port 3456
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Start web server as background daemon:**
|
|
118
|
+
```bash
|
|
119
|
+
mcp-config-manager start --port 3456
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Check daemon status:**
|
|
123
|
+
```bash
|
|
124
|
+
mcp-config-manager status
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Stop daemon:**
|
|
128
|
+
```bash
|
|
129
|
+
mcp-config-manager stop
|
|
109
130
|
```
|
|
110
131
|
|
|
111
132
|
Then open http://localhost:3456 in your browser.
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-config-manager",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Simple CLI and web UI to manage MCP configs across multiple AI clients",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
7
|
+
"mcp-config-manager": "src/cli.js",
|
|
7
8
|
"mcp-manager": "src/cli.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
package/src/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { MCPConfigManager } from './config-manager.js';
|
|
|
6
6
|
const manager = new MCPConfigManager();
|
|
7
7
|
|
|
8
8
|
program
|
|
9
|
-
.name('mcp-manager')
|
|
9
|
+
.name('mcp-config-manager')
|
|
10
10
|
.description('CLI to manage MCP configurations across different AI clients')
|
|
11
11
|
.version('1.0.0');
|
|
12
12
|
|
|
@@ -383,12 +383,108 @@ program
|
|
|
383
383
|
.command('web')
|
|
384
384
|
.description('Start the web UI server')
|
|
385
385
|
.option('-p, --port <port>', 'Port to run the server on', '3456')
|
|
386
|
+
.option('-d, --daemon', 'Run in background as daemon')
|
|
386
387
|
.action(async (options) => {
|
|
387
|
-
|
|
388
|
-
|
|
388
|
+
if (options.daemon) {
|
|
389
|
+
const { spawn } = await import('child_process');
|
|
390
|
+
const fs = await import('fs/promises');
|
|
391
|
+
const path = await import('path');
|
|
392
|
+
const os = await import('os');
|
|
393
|
+
|
|
394
|
+
const pidFile = path.join(os.tmpdir(), 'mcp-config-manager.pid');
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
// Check if daemon is already running
|
|
398
|
+
const existingPid = await fs.readFile(pidFile, 'utf8');
|
|
399
|
+
const { execSync } = await import('child_process');
|
|
400
|
+
try {
|
|
401
|
+
execSync(`kill -0 ${existingPid}`, { stdio: 'ignore' });
|
|
402
|
+
console.log(`Web UI server already running on port ${options.port} (PID: ${existingPid})`);
|
|
403
|
+
console.log(`Open http://localhost:${options.port} in your browser`);
|
|
404
|
+
return;
|
|
405
|
+
} catch {
|
|
406
|
+
// Process not running, remove stale pid file
|
|
407
|
+
await fs.unlink(pidFile);
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
// PID file doesn't exist
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Start daemon
|
|
414
|
+
const daemon = spawn(process.execPath, [process.argv[1], 'web', '--port', options.port], {
|
|
415
|
+
detached: true,
|
|
416
|
+
stdio: 'ignore'
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
daemon.unref();
|
|
420
|
+
await fs.writeFile(pidFile, daemon.pid.toString());
|
|
421
|
+
|
|
422
|
+
console.log(`Web UI server started as daemon on port ${options.port} (PID: ${daemon.pid})`);
|
|
423
|
+
console.log(`Open http://localhost:${options.port} in your browser`);
|
|
424
|
+
console.log(`Use 'mcp-config-manager stop' to stop the server`);
|
|
425
|
+
} else {
|
|
426
|
+
console.log(`Starting web UI server on port ${options.port}...`);
|
|
427
|
+
console.log(`Open http://localhost:${options.port} in your browser`);
|
|
428
|
+
|
|
429
|
+
const { startServer } = await import('./server.js');
|
|
430
|
+
startServer(parseInt(options.port));
|
|
431
|
+
}
|
|
432
|
+
});
|
|
389
433
|
|
|
390
|
-
|
|
391
|
-
|
|
434
|
+
program
|
|
435
|
+
.command('start')
|
|
436
|
+
.description('Start the web UI server in background')
|
|
437
|
+
.option('-p, --port <port>', 'Port to run the server on', '3456')
|
|
438
|
+
.action(async (options) => {
|
|
439
|
+
// Call web command with daemon option
|
|
440
|
+
program.parse([process.argv[0], process.argv[1], 'web', '--daemon', '--port', options.port]);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
program
|
|
444
|
+
.command('stop')
|
|
445
|
+
.description('Stop the background web UI server')
|
|
446
|
+
.action(async () => {
|
|
447
|
+
const fs = await import('fs/promises');
|
|
448
|
+
const path = await import('path');
|
|
449
|
+
const os = await import('os');
|
|
450
|
+
const { execSync } = await import('child_process');
|
|
451
|
+
|
|
452
|
+
const pidFile = path.join(os.tmpdir(), 'mcp-config-manager.pid');
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
const pid = await fs.readFile(pidFile, 'utf8');
|
|
456
|
+
execSync(`kill ${pid}`);
|
|
457
|
+
await fs.unlink(pidFile);
|
|
458
|
+
console.log(`Web UI server stopped (PID: ${pid})`);
|
|
459
|
+
} catch (error) {
|
|
460
|
+
console.log('No running web UI server found');
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
program
|
|
465
|
+
.command('status')
|
|
466
|
+
.description('Check status of background web UI server')
|
|
467
|
+
.action(async () => {
|
|
468
|
+
const fs = await import('fs/promises');
|
|
469
|
+
const path = await import('path');
|
|
470
|
+
const os = await import('os');
|
|
471
|
+
const { execSync } = await import('child_process');
|
|
472
|
+
|
|
473
|
+
const pidFile = path.join(os.tmpdir(), 'mcp-config-manager.pid');
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
const pid = await fs.readFile(pidFile, 'utf8');
|
|
477
|
+
try {
|
|
478
|
+
execSync(`kill -0 ${pid}`, { stdio: 'ignore' });
|
|
479
|
+
console.log(`Web UI server is running (PID: ${pid})`);
|
|
480
|
+
console.log(`Open http://localhost:3456 in your browser`);
|
|
481
|
+
} catch {
|
|
482
|
+
console.log('Web UI server is not running (stale PID file found)');
|
|
483
|
+
await fs.unlink(pidFile);
|
|
484
|
+
}
|
|
485
|
+
} catch {
|
|
486
|
+
console.log('Web UI server is not running');
|
|
487
|
+
}
|
|
392
488
|
});
|
|
393
489
|
|
|
394
490
|
program.parse();
|
package/src/config-manager.js
CHANGED
|
@@ -3,21 +3,46 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
|
|
5
5
|
import { CLIENTS as PROD_CLIENTS } from './clients.js';
|
|
6
|
-
import { MOCK_CLIENTS, MOCK_GLOBAL_SERVERS_PATH } from '../test/mock-clients.js';
|
|
7
6
|
|
|
8
7
|
const USE_MOCK_CLIENTS = process.env.MCP_USE_MOCK_CLIENTS === 'true';
|
|
9
|
-
const CLIENTS = USE_MOCK_CLIENTS ? MOCK_CLIENTS : PROD_CLIENTS;
|
|
10
|
-
const GLOBAL_SERVERS_PATH = USE_MOCK_CLIENTS ? MOCK_GLOBAL_SERVERS_PATH : path.join(os.homedir(), '.mcp-global-servers.json');
|
|
11
8
|
|
|
12
9
|
export class MCPConfigManager {
|
|
13
10
|
constructor() {
|
|
14
11
|
this.platform = os.platform();
|
|
15
12
|
this.availableClients = {};
|
|
13
|
+
this.clients = PROD_CLIENTS;
|
|
14
|
+
this.globalServersPath = path.join(os.homedir(), '.mcp-global-servers.json');
|
|
15
|
+
this.mockClientsInitialized = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async initializeMockClients() {
|
|
19
|
+
if (USE_MOCK_CLIENTS && !this.mockClientsInitialized) {
|
|
20
|
+
try {
|
|
21
|
+
const mockModule = await import('../test/mock-clients.js');
|
|
22
|
+
this.clients = mockModule.MOCK_CLIENTS;
|
|
23
|
+
this.globalServersPath = mockModule.MOCK_GLOBAL_SERVERS_PATH;
|
|
24
|
+
this.mockClientsInitialized = true;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.warn('Mock clients not available, using production clients instead');
|
|
27
|
+
// Fall back to production clients if mock clients are not available
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async getClients() {
|
|
33
|
+
await this.initializeMockClients();
|
|
34
|
+
return this.clients;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async getGlobalServersPath() {
|
|
38
|
+
await this.initializeMockClients();
|
|
39
|
+
return this.globalServersPath;
|
|
16
40
|
}
|
|
17
41
|
|
|
18
42
|
async readGlobalServers() {
|
|
19
43
|
try {
|
|
20
|
-
const
|
|
44
|
+
const globalPath = await this.getGlobalServersPath();
|
|
45
|
+
const content = await fs.readFile(globalPath, 'utf-8');
|
|
21
46
|
return JSON.parse(content);
|
|
22
47
|
} catch (error) {
|
|
23
48
|
if (error.code !== 'ENOENT') {
|
|
@@ -29,7 +54,8 @@ export class MCPConfigManager {
|
|
|
29
54
|
}
|
|
30
55
|
|
|
31
56
|
async writeGlobalServers(globalServers) {
|
|
32
|
-
await
|
|
57
|
+
const globalPath = await this.getGlobalServersPath();
|
|
58
|
+
await fs.writeFile(globalPath, JSON.stringify(globalServers, null, 2));
|
|
33
59
|
}
|
|
34
60
|
|
|
35
61
|
async addGlobalServer(serverName, serverConfig) {
|
|
@@ -103,10 +129,11 @@ export class MCPConfigManager {
|
|
|
103
129
|
configHash: this.getServerConfigHash(serverConfig || {})
|
|
104
130
|
};
|
|
105
131
|
}
|
|
132
|
+
const configPath = await this.getConfigPath(clientId);
|
|
106
133
|
allServers[serverName].clients.push({
|
|
107
134
|
id: clientId,
|
|
108
135
|
name: clientInfo.name,
|
|
109
|
-
configPath
|
|
136
|
+
configPath
|
|
110
137
|
});
|
|
111
138
|
// If a server exists globally and in a client, mark it as global
|
|
112
139
|
if (globalServers[serverName]) {
|
|
@@ -124,7 +151,8 @@ export class MCPConfigManager {
|
|
|
124
151
|
|
|
125
152
|
async detectClients() {
|
|
126
153
|
const detectedClients = {};
|
|
127
|
-
|
|
154
|
+
const clients = await this.getClients();
|
|
155
|
+
for (const [id, client] of Object.entries(clients)) {
|
|
128
156
|
const configPath = client.configPaths[os.platform()];
|
|
129
157
|
const absoluteConfigPath = path.isAbsolute(configPath) ? configPath : path.join(process.cwd(), configPath);
|
|
130
158
|
try {
|
|
@@ -153,19 +181,21 @@ export class MCPConfigManager {
|
|
|
153
181
|
try {
|
|
154
182
|
const config = await this.readConfig(key);
|
|
155
183
|
const serverCount = Object.keys(config.servers).length;
|
|
184
|
+
const configPath = await this.getConfigPath(key);
|
|
156
185
|
clientsWithConfigs.push({
|
|
157
186
|
id: key,
|
|
158
187
|
name: client.name,
|
|
159
|
-
configPath
|
|
188
|
+
configPath,
|
|
160
189
|
serverCount,
|
|
161
190
|
exists: true
|
|
162
191
|
});
|
|
163
192
|
} catch (error) {
|
|
164
193
|
console.error(`Error processing client ${key}:`, error.message);
|
|
194
|
+
const configPath = await this.getConfigPath(key);
|
|
165
195
|
clientsWithConfigs.push({
|
|
166
196
|
id: key,
|
|
167
197
|
name: client.name,
|
|
168
|
-
configPath
|
|
198
|
+
configPath,
|
|
169
199
|
serverCount: 0,
|
|
170
200
|
exists: false
|
|
171
201
|
});
|
|
@@ -175,8 +205,9 @@ export class MCPConfigManager {
|
|
|
175
205
|
return clientsWithConfigs;
|
|
176
206
|
}
|
|
177
207
|
|
|
178
|
-
getConfigPath(client) {
|
|
179
|
-
const
|
|
208
|
+
async getConfigPath(client) {
|
|
209
|
+
const clients = await this.getClients();
|
|
210
|
+
const clientConfig = clients[client];
|
|
180
211
|
if (!clientConfig) {
|
|
181
212
|
throw new Error(`Unknown client: ${client}`);
|
|
182
213
|
}
|
|
@@ -189,13 +220,14 @@ export class MCPConfigManager {
|
|
|
189
220
|
}
|
|
190
221
|
|
|
191
222
|
async readConfig(client) {
|
|
192
|
-
const configPath = this.getConfigPath(client);
|
|
223
|
+
const configPath = await this.getConfigPath(client);
|
|
193
224
|
let clientConfig = { servers: {} };
|
|
194
225
|
|
|
195
226
|
try {
|
|
196
227
|
const content = await fs.readFile(configPath, 'utf-8');
|
|
197
228
|
const parsedContent = JSON.parse(content);
|
|
198
|
-
|
|
229
|
+
const clients = await this.getClients();
|
|
230
|
+
clientConfig = this.normalizeConfig(parsedContent, clients[client].format);
|
|
199
231
|
} catch (error) {
|
|
200
232
|
if (error.code !== 'ENOENT') {
|
|
201
233
|
throw error;
|
|
@@ -239,8 +271,9 @@ export class MCPConfigManager {
|
|
|
239
271
|
}
|
|
240
272
|
|
|
241
273
|
async writeConfig(client, config) {
|
|
242
|
-
const configPath = this.getConfigPath(client);
|
|
243
|
-
const
|
|
274
|
+
const configPath = await this.getConfigPath(client);
|
|
275
|
+
const clients = await this.getClients();
|
|
276
|
+
const clientConfig = clients[client];
|
|
244
277
|
|
|
245
278
|
let originalConfig = {};
|
|
246
279
|
try {
|
|
@@ -396,8 +429,9 @@ export class MCPConfigManager {
|
|
|
396
429
|
|
|
397
430
|
async getAllEnvironmentVariables() {
|
|
398
431
|
const envVarMap = new Map();
|
|
432
|
+
const clients = await this.getClients();
|
|
399
433
|
|
|
400
|
-
for (const [clientId, clientInfo] of Object.entries(
|
|
434
|
+
for (const [clientId, clientInfo] of Object.entries(clients)) {
|
|
401
435
|
try {
|
|
402
436
|
const config = await this.readConfig(clientId);
|
|
403
437
|
|
|
@@ -430,8 +464,9 @@ export class MCPConfigManager {
|
|
|
430
464
|
|
|
431
465
|
async updateEnvironmentVariableAcrossConfigs(envKey, newValue, targetServers = null) {
|
|
432
466
|
const results = [];
|
|
467
|
+
const clients = await this.getClients();
|
|
433
468
|
|
|
434
|
-
for (const [clientId, clientInfo] of Object.entries(
|
|
469
|
+
for (const [clientId, clientInfo] of Object.entries(clients)) {
|
|
435
470
|
try {
|
|
436
471
|
const config = await this.readConfig(clientId);
|
|
437
472
|
let configModified = false;
|