mcp-config-manager 1.0.2

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.
@@ -0,0 +1,520 @@
1
+ import * as fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ import { CLIENTS as PROD_CLIENTS } from './clients.js';
6
+ import { MOCK_CLIENTS, MOCK_GLOBAL_SERVERS_PATH } from '../test/mock-clients.js';
7
+
8
+ 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
+
12
+ export class MCPConfigManager {
13
+ constructor() {
14
+ this.platform = os.platform();
15
+ this.availableClients = {};
16
+ }
17
+
18
+ async readGlobalServers() {
19
+ try {
20
+ const content = await fs.readFile(GLOBAL_SERVERS_PATH, 'utf-8');
21
+ return JSON.parse(content);
22
+ } catch (error) {
23
+ if (error.code !== 'ENOENT') {
24
+ throw error;
25
+ }
26
+ // If file doesn't exist, return empty object
27
+ return {};
28
+ }
29
+ }
30
+
31
+ async writeGlobalServers(globalServers) {
32
+ await fs.writeFile(GLOBAL_SERVERS_PATH, JSON.stringify(globalServers, null, 2));
33
+ }
34
+
35
+ async addGlobalServer(serverName, serverConfig) {
36
+ const globalServers = await this.readGlobalServers();
37
+ globalServers[serverName] = serverConfig;
38
+ await this.writeGlobalServers(globalServers);
39
+ }
40
+
41
+ async removeGlobalServer(serverName) {
42
+ const globalServers = await this.readGlobalServers();
43
+ delete globalServers[serverName];
44
+ await this.writeGlobalServers(globalServers);
45
+ }
46
+
47
+ async updateGlobalServerEnv(serverName, envKey, envValue) {
48
+ const globalServers = await this.readGlobalServers();
49
+ if (!globalServers[serverName]) {
50
+ throw new Error(`Global server ${serverName} not found`);
51
+ }
52
+
53
+ if (!globalServers[serverName].env) {
54
+ globalServers[serverName].env = {};
55
+ }
56
+
57
+ if (envValue === null || envValue === undefined) {
58
+ delete globalServers[serverName].env[envKey];
59
+ } else {
60
+ globalServers[serverName].env[envKey] = envValue;
61
+ }
62
+
63
+ await this.writeGlobalServers(globalServers);
64
+ }
65
+
66
+ async getAllServers() {
67
+ return this.readGlobalServers();
68
+ }
69
+
70
+ // Helper to generate a consistent hash for a server config
71
+ getServerConfigHash(config) {
72
+ // Exclude env for now, or handle it specially if exact env matching is needed
73
+ const { env, ...rest } = config;
74
+ return JSON.stringify(rest);
75
+ }
76
+
77
+ async getServersInClients() {
78
+ const allServers = {}; // serverName: { clients: [{id, name, configPath}], global: boolean, config: {}, configHash: string }
79
+ const globalServers = await this.readGlobalServers();
80
+
81
+ // Add global servers first
82
+ for (const [serverName, serverConfig] of Object.entries(globalServers)) {
83
+ allServers[serverName] = {
84
+ clients: [],
85
+ global: true,
86
+ config: serverConfig,
87
+ configHash: this.getServerConfigHash(serverConfig)
88
+ };
89
+ }
90
+
91
+ const availableClients = await this.getAvailableClients();
92
+
93
+ for (const [clientId, clientInfo] of Object.entries(availableClients)) {
94
+ try {
95
+ const clientConfig = await this.readConfig(clientId);
96
+
97
+ for (const [serverName, serverConfig] of Object.entries(clientConfig.servers)) {
98
+ if (!allServers[serverName]) {
99
+ allServers[serverName] = {
100
+ clients: [],
101
+ global: false, // Will be true if it's also in globalServers
102
+ config: serverConfig || {}, // Ensure config is an object
103
+ configHash: this.getServerConfigHash(serverConfig || {})
104
+ };
105
+ }
106
+ allServers[serverName].clients.push({
107
+ id: clientId,
108
+ name: clientInfo.name,
109
+ configPath: this.getConfigPath(clientId)
110
+ });
111
+ // If a server exists globally and in a client, mark it as global
112
+ if (globalServers[serverName]) {
113
+ allServers[serverName].global = true;
114
+ }
115
+ }
116
+ } catch (error) {
117
+ console.error(`Error in getServersInClients for client ${clientId}:`, error);
118
+ // Client config might not exist, or other read error, skip
119
+ console.warn(`Could not read config for client ${clientId}: ${error.message}`);
120
+ }
121
+ }
122
+ return allServers;
123
+ }
124
+
125
+ async detectClients() {
126
+ const detectedClients = {};
127
+ for (const [id, client] of Object.entries(CLIENTS)) {
128
+ const configPath = client.configPaths[os.platform()];
129
+ const absoluteConfigPath = path.isAbsolute(configPath) ? configPath : path.join(process.cwd(), configPath);
130
+ try {
131
+ await fs.access(absoluteConfigPath, fs.constants.F_OK);
132
+ detectedClients[id] = client;
133
+ } catch (error) {
134
+ // File does not exist or is not accessible, skip this client
135
+ }
136
+ }
137
+ this.availableClients = detectedClients;
138
+ return detectedClients;
139
+ }
140
+
141
+ async getAvailableClients() {
142
+ if (Object.keys(this.availableClients).length === 0) {
143
+ await this.detectClients();
144
+ }
145
+ return this.availableClients;
146
+ }
147
+
148
+ async listClients() {
149
+ const clientsWithConfigs = [];
150
+ const availableClients = await this.getAvailableClients();
151
+
152
+ for (const [key, client] of Object.entries(availableClients)) {
153
+ try {
154
+ const config = await this.readConfig(key);
155
+ const serverCount = Object.keys(config.servers).length;
156
+ clientsWithConfigs.push({
157
+ id: key,
158
+ name: client.name,
159
+ configPath: this.getConfigPath(key),
160
+ serverCount,
161
+ exists: true
162
+ });
163
+ } catch (error) {
164
+ console.error(`Error processing client ${key}:`, error.message);
165
+ clientsWithConfigs.push({
166
+ id: key,
167
+ name: client.name,
168
+ configPath: this.getConfigPath(key),
169
+ serverCount: 0,
170
+ exists: false
171
+ });
172
+ }
173
+ }
174
+
175
+ return clientsWithConfigs;
176
+ }
177
+
178
+ getConfigPath(client) {
179
+ const clientConfig = CLIENTS[client];
180
+ if (!clientConfig) {
181
+ throw new Error(`Unknown client: ${client}`);
182
+ }
183
+
184
+ const platformKey = this.platform === 'darwin' ? 'darwin' :
185
+ this.platform === 'win32' ? 'win32' : 'linux';
186
+
187
+ const configPath = clientConfig.configPaths[platformKey];
188
+ return configPath;
189
+ }
190
+
191
+ async readConfig(client) {
192
+ const configPath = this.getConfigPath(client);
193
+ let clientConfig = { servers: {} };
194
+
195
+ try {
196
+ const content = await fs.readFile(configPath, 'utf-8');
197
+ const parsedContent = JSON.parse(content);
198
+ clientConfig = this.normalizeConfig(parsedContent, CLIENTS[client].format);
199
+ } catch (error) {
200
+ if (error.code !== 'ENOENT') {
201
+ throw error;
202
+ }
203
+ // If file doesn't exist, clientConfig remains { servers: {} }
204
+ }
205
+
206
+ const globalServers = await this.readGlobalServers();
207
+
208
+ // Merge global servers into clientConfig, client-specific overrides global
209
+ const mergedServers = { ...globalServers };
210
+ for (const [serverName, serverDetails] of Object.entries(clientConfig.servers)) {
211
+ mergedServers[serverName] = { ...mergedServers[serverName], ...serverDetails };
212
+ }
213
+
214
+ return { servers: mergedServers };
215
+ }
216
+
217
+ normalizeConfig(config, format) {
218
+ if (format === 'mcpServers') {
219
+ return { servers: config.mcpServers || {} };
220
+ } else if (format === 'mcp.servers') {
221
+ return { servers: config.mcp?.servers || {} };
222
+ }
223
+ return { servers: {} };
224
+ }
225
+
226
+ denormalizeConfig(normalizedConfig, format, originalConfig = {}) {
227
+ if (format === 'mcpServers') {
228
+ return { ...originalConfig, mcpServers: normalizedConfig.servers };
229
+ } else if (format === 'mcp.servers') {
230
+ return {
231
+ ...originalConfig,
232
+ mcp: {
233
+ ...(originalConfig.mcp || {}),
234
+ servers: normalizedConfig.servers
235
+ }
236
+ };
237
+ }
238
+ return originalConfig;
239
+ }
240
+
241
+ async writeConfig(client, config) {
242
+ const configPath = this.getConfigPath(client);
243
+ const clientConfig = CLIENTS[client];
244
+
245
+ let originalConfig = {};
246
+ try {
247
+ const content = await fs.readFile(configPath, 'utf-8');
248
+ originalConfig = JSON.parse(content);
249
+ } catch (error) {
250
+ // File doesn't exist, that's OK
251
+ }
252
+
253
+ const finalConfig = this.denormalizeConfig(config, clientConfig.format, originalConfig);
254
+
255
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
256
+ await fs.writeFile(configPath, JSON.stringify(finalConfig, null, 2));
257
+ }
258
+
259
+
260
+
261
+ async addServer(client, serverName, serverConfig) {
262
+ const config = await this.readConfig(client);
263
+ config.servers[serverName] = serverConfig;
264
+ await this.writeConfig(client, config);
265
+ }
266
+
267
+ async addServerToMultipleClients(serverName, serverConfig, clientIds) {
268
+ const results = [];
269
+ for (const clientId of clientIds) {
270
+ try {
271
+ await this.addServer(clientId, serverName, serverConfig);
272
+ results.push({ client: clientId, server: serverName, success: true });
273
+ } catch (error) {
274
+ results.push({ client: clientId, server: serverName, success: false, error: error.message });
275
+ }
276
+ }
277
+ return results;
278
+ }
279
+
280
+ async updateServerInMultipleClients(serverName, serverConfig, clientIds) {
281
+ const results = [];
282
+ for (const clientId of clientIds) {
283
+ try {
284
+ const config = await this.readConfig(clientId);
285
+ config.servers[serverName] = serverConfig;
286
+ await this.writeConfig(clientId, config);
287
+ results.push({ client: clientId, server: serverName, success: true });
288
+ } catch (error) {
289
+ results.push({ client: clientId, server: serverName, success: false, error: error.message });
290
+ }
291
+ }
292
+ return results;
293
+ }
294
+
295
+ async removeServer(client, serverName) {
296
+ const config = await this.readConfig(client);
297
+ delete config.servers[serverName];
298
+ await this.writeConfig(client, config);
299
+ }
300
+
301
+ async updateServerEnv(client, serverName, envKey, envValue) {
302
+ const config = await this.readConfig(client);
303
+ if (!config.servers[serverName]) {
304
+ throw new Error(`Server ${serverName} not found in ${client}`);
305
+ }
306
+
307
+ if (!config.servers[serverName].env) {
308
+ config.servers[serverName].env = {};
309
+ }
310
+
311
+ if (envValue === null || envValue === undefined) {
312
+ delete config.servers[serverName].env[envKey];
313
+ }
314
+ else {
315
+ config.servers[serverName].env[envKey] = envValue;
316
+ }
317
+
318
+ await this.writeConfig(client, config);
319
+ }
320
+
321
+ async copyServer(fromClient, fromServerName, toClient, toServerName = null) {
322
+ const fromConfig = await this.readConfig(fromClient);
323
+ const serverConfig = fromConfig.servers[fromServerName];
324
+
325
+ if (!serverConfig) {
326
+ throw new Error(`Server ${fromServerName} not found in ${fromClient}`);
327
+ }
328
+
329
+ const targetName = toServerName || fromServerName;
330
+ await this.addServer(toClient, targetName, serverConfig);
331
+ }
332
+
333
+ async exportConfig(client, outputPath = null) {
334
+ const config = await this.readConfig(client);
335
+ const exportData = {
336
+ client,
337
+ timestamp: new Date().toISOString(),
338
+ servers: config.servers
339
+ };
340
+
341
+ if (outputPath) {
342
+ await fs.writeFile(outputPath, JSON.stringify(exportData, null, 2));
343
+ return outputPath;
344
+ }
345
+ else {
346
+ return exportData;
347
+ }
348
+ }
349
+
350
+ async exportServer(client, serverName, outputPath = null) {
351
+ const config = await this.readConfig(client);
352
+ const serverConfig = config.servers[serverName];
353
+
354
+ if (!serverConfig) {
355
+ throw new Error(`Server ${serverName} not found in ${client}`);
356
+ }
357
+
358
+ const exportData = {
359
+ client,
360
+ serverName,
361
+ timestamp: new Date().toISOString(),
362
+ config: serverConfig
363
+ };
364
+
365
+ if (outputPath) {
366
+ await fs.writeFile(outputPath, JSON.stringify(exportData, null, 2));
367
+ return outputPath;
368
+ }
369
+ else {
370
+ return exportData;
371
+ }
372
+ }
373
+
374
+ async importConfig(client, importPath) {
375
+ const content = await fs.readFile(importPath, 'utf-8');
376
+ const importData = JSON.parse(content);
377
+
378
+ if (importData.servers) {
379
+ await this.writeConfig(client, { servers: importData.servers });
380
+ }
381
+ else if (importData.config) {
382
+ await this.addServer(client, importData.serverName, importData.config);
383
+ }
384
+ else {
385
+ throw new Error('Invalid import file format');
386
+ }
387
+ }
388
+
389
+ async getSupportedClients() {
390
+ const availableClients = await this.getAvailableClients();
391
+ return Object.entries(availableClients).map(([id, client]) => ({
392
+ id,
393
+ name: client.name,
394
+ }));
395
+ }
396
+
397
+ async getAllEnvironmentVariables() {
398
+ const envVarMap = new Map();
399
+
400
+ for (const [clientId, clientInfo] of Object.entries(CLIENTS)) {
401
+ try {
402
+ const config = await this.readConfig(clientId);
403
+
404
+ for (const [serverName, serverConfig] of Object.entries(config.servers)) {
405
+ if (serverConfig.env) {
406
+ for (const [envKey, envValue] of Object.entries(serverConfig.env)) {
407
+ if (!envVarMap.has(envKey)) {
408
+ envVarMap.set(envKey, {
409
+ key: envKey,
410
+ locations: []
411
+ });
412
+ }
413
+
414
+ envVarMap.get(envKey).locations.push({
415
+ client: clientId,
416
+ clientName: clientInfo.name,
417
+ server: serverName,
418
+ value: envValue
419
+ });
420
+ }
421
+ }
422
+ }
423
+ } catch (error) {
424
+ // Skip clients without configs
425
+ }
426
+ }
427
+
428
+ return Array.from(envVarMap.values()).sort((a, b) => a.key.localeCompare(b.key));
429
+ }
430
+
431
+ async updateEnvironmentVariableAcrossConfigs(envKey, newValue, targetServers = null) {
432
+ const results = [];
433
+
434
+ for (const [clientId, clientInfo] of Object.entries(CLIENTS)) {
435
+ try {
436
+ const config = await this.readConfig(clientId);
437
+ let configModified = false;
438
+
439
+ for (const [serverName, serverConfig] of Object.entries(config.servers)) {
440
+ if (serverConfig.env && serverConfig.env.hasOwnProperty(envKey)) {
441
+ // If targetServers is specified, only update those
442
+ if (targetServers && !targetServers.some(t =>
443
+ t.client === clientId && t.server === serverName)) {
444
+ continue;
445
+ }
446
+
447
+ const oldValue = serverConfig.env[envKey];
448
+
449
+ if (newValue === null || newValue === undefined) {
450
+ delete serverConfig.env[envKey];
451
+ }
452
+ else {
453
+ serverConfig.env[envKey] = newValue;
454
+ }
455
+
456
+ configModified = true;
457
+
458
+ results.push({
459
+ client: clientId,
460
+ clientName: clientInfo.name,
461
+ server: serverName,
462
+ oldValue,
463
+ newValue,
464
+ success: true
465
+ });
466
+ }
467
+ }
468
+
469
+ if (configModified) {
470
+ await this.writeConfig(clientId, config);
471
+ }
472
+ } catch (error) {
473
+ results.push({
474
+ client: clientId,
475
+ clientName: clientInfo.name,
476
+ error: error.message,
477
+ success: false
478
+ });
479
+ }
480
+ }
481
+
482
+ return results;
483
+ }
484
+
485
+ async renameServerAcrossClients(oldName, newName) {
486
+ if (oldName === newName) {
487
+ return { success: true, message: "Server name is the same, no action taken." };
488
+ }
489
+
490
+ const results = { global: false, clients: [] };
491
+
492
+ // Handle global servers
493
+ const globalServers = await this.readGlobalServers();
494
+ if (globalServers[oldName]) {
495
+ globalServers[newName] = globalServers[oldName];
496
+ delete globalServers[oldName];
497
+ await this.writeGlobalServers(globalServers);
498
+ results.global = true;
499
+ }
500
+
501
+ // Handle clients
502
+ const availableClients = await this.getAvailableClients();
503
+ for (const [clientId, clientInfo] of Object.entries(availableClients)) {
504
+ try {
505
+ const config = await this.readConfig(clientId);
506
+ if (config.servers[oldName]) {
507
+ config.servers[newName] = config.servers[oldName];
508
+ delete config.servers[oldName];
509
+ await this.writeConfig(clientId, config);
510
+ results.clients.push({ id: clientId, name: clientInfo.name, success: true });
511
+ } else {
512
+ results.clients.push({ id: clientId, name: clientInfo.name, success: false, message: "Server not found in client config." });
513
+ }
514
+ } catch (error) {
515
+ results.clients.push({ id: clientId, name: clientInfo.name, success: false, error: error.message });
516
+ }
517
+ }
518
+ return results;
519
+ }
520
+ }