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,822 @@
1
+ import { addEnvVarRow, switchTab, buildConfigFromForm, updateFormFromConfig } from './utils.js';
2
+ import {
3
+ addServerApi, updateServerApi, deleteServerApi, copyServerApi,
4
+ exportConfigApi, exportServerApi, importConfigApi, getClientConfigApi, renameServerApi, updateServerEnvApi, updateServerInClientsApi
5
+ } from './api.js';
6
+
7
+ let clients = []; // This will be passed from main.js
8
+ let currentClient = null; // This will be passed from main.js
9
+ let loadClientsCallback = null; // Callback to main.js to reload all clients
10
+
11
+ export function initModals(clientData, currentClientData, loadClientsFn) {
12
+ clients = clientData;
13
+ currentClient = currentClientData;
14
+ loadClientsCallback = loadClientsFn;
15
+ }
16
+
17
+ export function showServerModal(serverName = null, serverConfig = null, loadClientServers, renderKanbanBoard, clientId = null, loadClientsFn) {
18
+ const modal = document.getElementById('serverModal');
19
+ const title = document.getElementById('modalTitle');
20
+ const form = document.getElementById('serverForm');
21
+
22
+ // Store the client ID in the modal's data attribute to preserve it
23
+ if (clientId) {
24
+ if (Array.isArray(clientId)) {
25
+ modal.dataset.clientIds = JSON.stringify(clientId);
26
+ delete modal.dataset.clientId; // Ensure single client id is not set
27
+ } else {
28
+ currentClient = clientId;
29
+ modal.dataset.clientId = clientId;
30
+ delete modal.dataset.clientIds; // Ensure multiple client ids are not set
31
+ }
32
+ } else if (!currentClient && modal.dataset.clientId) {
33
+ // Restore from modal's data attribute if currentClient is null
34
+ currentClient = modal.dataset.clientId;
35
+ }
36
+
37
+ title.textContent = serverName ? 'Edit Server' : 'Add Server';
38
+
39
+ document.getElementById('serverName').value = serverName || '';
40
+ document.getElementById('serverName').disabled = false; // Always editable
41
+
42
+
43
+ document.getElementById('serverCommand').value = serverConfig?.command || '';
44
+ document.getElementById('serverArgs').value = serverConfig?.args?.join('\n') || '';
45
+
46
+ // Set JSON editor content
47
+ if (!serverName && !serverConfig) {
48
+ // For new servers, show the expected format
49
+ document.getElementById('jsonEditor').value = JSON.stringify({
50
+ "new-server": {
51
+ "command": "npx",
52
+ "args": ["-y", "@example/mcp-server"],
53
+ "env": {}
54
+ }
55
+ }, null, 2);
56
+ } else {
57
+ document.getElementById('jsonEditor').value = JSON.stringify(serverConfig || {}, null, 2);
58
+ }
59
+
60
+ const envVarsDiv = document.getElementById('envVars');
61
+ envVarsDiv.innerHTML = '';
62
+ const loadedEnvKeys = [];
63
+
64
+ if (serverConfig?.env) {
65
+ for (const entry of Object.entries(serverConfig.env)) {
66
+ const key = entry[0];
67
+ const value = entry[1];
68
+ addEnvVarRow(envVarsDiv, key, value);
69
+ loadedEnvKeys.push(key);
70
+ }
71
+ }
72
+ envVarsDiv.dataset.initialKeys = JSON.stringify(loadedEnvKeys);
73
+
74
+ document.getElementById('addEnvVar').removeEventListener('click', addEnvVarRow);
75
+ const addEnvVarButton = document.getElementById('addEnvVar');
76
+ addEnvVarButton.onclick = null; // Clear any previous handlers
77
+ addEnvVarButton.onclick = () => addEnvVarRow(envVarsDiv);
78
+
79
+
80
+ // Attach event listeners to dynamically added 'Copy' buttons for individual env vars
81
+ envVarsDiv.addEventListener('click', async (e) => {
82
+ if (e.target.classList.contains('copy-env-var-btn')) {
83
+ const envKey = e.target.dataset.key;
84
+ const envValue = e.target.dataset.value;
85
+ // Get the latest clients list
86
+ const { listClientsApi } = await import('./api.js');
87
+ const allClients = await listClientsApi();
88
+ await showCopySingleEnvVarModal(serverName, envKey, envValue, clientId, allClients, loadClientsCallback || loadClientsFn);
89
+ }
90
+ });
91
+
92
+ // Setup tab switching
93
+ document.querySelectorAll('.tab-btn').forEach(btn => {
94
+ btn.onclick = () => switchTab(btn.dataset.tab);
95
+ });
96
+
97
+ modal.style.display = 'flex';
98
+ document.getElementById('serverName').focus();
99
+
100
+ form.onsubmit = async (e) => {
101
+ e.preventDefault();
102
+ const modalClient = modal.dataset.clientId || currentClient;
103
+ const modalClients = modal.dataset.clientIds ? JSON.parse(modal.dataset.clientIds) : null;
104
+
105
+ if (!modalClient && !modalClients) {
106
+ alert('No client selected. Please close this modal and select a client first.');
107
+ return;
108
+ }
109
+ await saveServer(serverName, loadClientServers, renderKanbanBoard, loadClientsFn);
110
+ };
111
+ }
112
+
113
+ async function saveServer(originalName, loadClientServers, renderKanbanBoard, loadClientsFn) {
114
+ const modal = document.getElementById('serverModal');
115
+ const clientToUse = modal.dataset.clientId || currentClient;
116
+ const clientsToUse = modal.dataset.clientIds ? JSON.parse(modal.dataset.clientIds) : null;
117
+
118
+ if (!clientToUse && !clientsToUse) {
119
+ alert('No client selected. Please close this modal and try again.');
120
+ return;
121
+ }
122
+
123
+ let newServerName;
124
+ let serverConfig;
125
+ const activeTab = document.querySelector('.tab-btn.active').dataset.tab;
126
+
127
+ if (activeTab === 'json') {
128
+ try {
129
+ const jsonData = JSON.parse(document.getElementById('jsonEditor').value);
130
+
131
+ // Check if this is a new server (originalName is null)
132
+ if (!originalName) {
133
+ // For new servers in JSON mode, expect format: { "serverName": { config } }
134
+ const keys = Object.keys(jsonData);
135
+ if (keys.length === 0) {
136
+ alert('Server configuration cannot be empty. Use format: { "serverName": { "command": "...", "args": [...] } }');
137
+ return;
138
+ }
139
+ if (keys.length > 1) {
140
+ alert('Only one server can be added at a time. Use format: { "serverName": { "command": "...", "args": [...] } }');
141
+ return;
142
+ }
143
+ newServerName = keys[0];
144
+ serverConfig = jsonData[newServerName];
145
+ } else {
146
+ // For editing existing servers, use the name from the input field
147
+ serverConfig = jsonData;
148
+ newServerName = document.getElementById('serverName').value;
149
+ if (!newServerName) {
150
+ alert('Server name cannot be empty.');
151
+ return;
152
+ }
153
+ }
154
+ } catch (e) {
155
+ alert('Invalid JSON format');
156
+ return;
157
+ }
158
+ } else {
159
+ const nameInput = document.getElementById('serverName');
160
+ newServerName = nameInput.value;
161
+
162
+ if (!newServerName) {
163
+ alert('Server name cannot be empty.');
164
+ return;
165
+ }
166
+
167
+ try {
168
+ const existingJson = JSON.parse(document.getElementById('jsonEditor').value);
169
+ const formConfig = buildConfigFromForm();
170
+ const mergedConfig = { ...existingJson };
171
+
172
+ if (formConfig.command) {
173
+ mergedConfig.command = formConfig.command;
174
+ } else if (document.getElementById('serverCommand').value === '') {
175
+ delete mergedConfig.command;
176
+ }
177
+
178
+ if (formConfig.args && formConfig.args.length > 0) {
179
+ mergedConfig.args = formConfig.args;
180
+ } else if (document.getElementById('serverArgs').value === '') {
181
+ delete mergedConfig.args;
182
+ }
183
+
184
+ if (mergedConfig.env || formConfig.env) {
185
+ const updatedEnv = { ...(mergedConfig.env || {}) };
186
+ const initialFormKeys = document.getElementById('envVars').dataset.initialKeys;
187
+ const initialKeys = initialFormKeys ? JSON.parse(initialFormKeys) : [];
188
+ const initialKeysSet = new Set(initialKeys);
189
+ const currentFormEnvs = {};
190
+ const currentFormKeys = new Set();
191
+ document.querySelectorAll('.env-var-row').forEach(row => {
192
+ const key = row.querySelector('.env-key').value;
193
+ const value = row.querySelector('.env-value').value;
194
+ if (key) {
195
+ currentFormKeys.add(key);
196
+ currentFormEnvs[key] = value;
197
+ }
198
+ });
199
+
200
+ initialKeys.forEach(key => {
201
+ if (currentFormKeys.has(key)) {
202
+ updatedEnv[key] = currentFormEnvs[key];
203
+ } else {
204
+ delete updatedEnv[key];
205
+ }
206
+ });
207
+
208
+ currentFormKeys.forEach(key => {
209
+ if (!initialKeysSet.has(key)) {
210
+ updatedEnv[key] = currentFormEnvs[key];
211
+ }
212
+ });
213
+
214
+ if (Object.keys(updatedEnv).length > 0) {
215
+ mergedConfig.env = updatedEnv;
216
+ } else {
217
+ delete mergedConfig.env;
218
+ }
219
+ }
220
+ serverConfig = mergedConfig;
221
+ } catch (e) {
222
+ alert('Could not merge configurations. Please check the JSON tab for errors.');
223
+ return;
224
+ }
225
+ }
226
+
227
+ if (originalName && newServerName !== originalName) {
228
+ // Rename operation
229
+ try {
230
+ const renameResponse = await renameServerApi(originalName, newServerName);
231
+ if (!renameResponse.success) {
232
+ throw new Error('Failed to rename server');
233
+ }
234
+ } catch (error) {
235
+ alert('Failed to rename server: ' + error.message);
236
+ return;
237
+ }
238
+ }
239
+
240
+ const serverToSaveName = newServerName;
241
+
242
+ try {
243
+ let response;
244
+ if (clientsToUse) {
245
+ response = await updateServerInClientsApi(serverToSaveName, serverConfig, clientsToUse);
246
+ } else if (originalName) {
247
+ response = await updateServerApi(clientToUse, serverToSaveName, serverConfig);
248
+ } else {
249
+ response = await addServerApi(clientToUse, serverToSaveName, serverConfig);
250
+ }
251
+
252
+ if (!response.success) {
253
+ throw new Error(`Failed to save server`);
254
+ }
255
+
256
+ document.getElementById('serverModal').style.display = 'none';
257
+ await loadClientServers();
258
+ if (renderKanbanBoard) {
259
+ await renderKanbanBoard();
260
+ }
261
+ loadClientsFn();
262
+ } catch (error) {
263
+ alert('Failed to save server: ' + error.message);
264
+ }
265
+ }
266
+
267
+ export async function editServer(name, loadClientServers, clientId = null, loadClientsFn = null) {
268
+ try {
269
+ const clientToUse = clientId || currentClient;
270
+ if (!clientToUse) {
271
+ throw new Error('No client selected for editing.');
272
+ }
273
+ const config = await getClientConfigApi(Array.isArray(clientToUse) ? clientToUse[0] : clientToUse);
274
+ showServerModal(name, config.servers[name], loadClientServers, loadClientServers, clientToUse, loadClientsFn);
275
+ } catch (error) {
276
+ alert('Failed to load server config: ' + error.message);
277
+ }
278
+ }
279
+
280
+ export async function deleteServer(name, loadClientServers, renderKanbanBoard, loadClientsFn, clientId = null) {
281
+ if (!confirm(`Are you sure you want to delete the server "${name}"?`)) {
282
+ return;
283
+ }
284
+
285
+ try {
286
+ const clientToDeleteFrom = clientId || currentClient;
287
+ const response = await deleteServerApi(clientToDeleteFrom, name);
288
+
289
+ if (response.success) {
290
+ await loadClientServers();
291
+ if (renderKanbanBoard) {
292
+ await renderKanbanBoard();
293
+ }
294
+ loadClientsFn();
295
+ loadClientsFn();
296
+ } else {
297
+ throw new Error('Failed to delete server');
298
+ }
299
+ } catch (error) {
300
+ alert('Failed to delete server: ' + error.message);
301
+ }
302
+ }
303
+
304
+ // ... (the rest of the file is unchanged)
305
+
306
+ export function copyServer(serverName, loadClientsFn) {
307
+ const modal = document.getElementById('copyModal');
308
+ const form = document.getElementById('copyForm');
309
+ const targetClientsDiv = document.getElementById('targetClients');
310
+
311
+ targetClientsDiv.innerHTML = '';
312
+ clients.forEach(client => {
313
+ if (client.id !== currentClient) {
314
+ const item = document.createElement('div');
315
+ item.className = 'checkbox-item';
316
+ item.innerHTML = `
317
+ <input type="checkbox" id="target-${client.id}" value="${client.id}">
318
+ <label for="target-${client.id}">${client.name}</label>
319
+ `;
320
+ targetClientsDiv.appendChild(item);
321
+ }
322
+ });
323
+
324
+ modal.style.display = 'flex';
325
+
326
+ form.onsubmit = async (e) => {
327
+ e.preventDefault();
328
+ const targetClients = [];
329
+ targetClientsDiv.querySelectorAll('input:checked').forEach(input => {
330
+ targetClients.push(input.value);
331
+ });
332
+
333
+ if (targetClients.length === 0) {
334
+ alert('Please select at least one target client');
335
+ return;
336
+ }
337
+
338
+ const targetName = document.getElementById('targetServerName').value || serverName;
339
+
340
+ try {
341
+ for (const targetClient of targetClients) {
342
+ const response = await copyServerApi(currentClient, serverName, targetClient, targetName);
343
+
344
+ if (!response.success) {
345
+ throw new Error(`Failed to copy to ${targetClient}`);
346
+ }
347
+ }
348
+
349
+ modal.style.display = 'none';
350
+ loadClientsFn();
351
+ } catch (error) {
352
+ alert('Failed to copy server: ' + error.message);
353
+ }
354
+ };
355
+ }
356
+
357
+ export async function copyToClipboard(serverName, event, serverConfig = null, clientId = null) {
358
+ try {
359
+ let configToCopy = serverConfig;
360
+ if (!configToCopy) {
361
+ // Use the passed clientId if available, otherwise fall back to currentClient
362
+ const clientToUse = clientId || currentClient;
363
+ if (!clientToUse) {
364
+ throw new Error('No client specified for copy operation');
365
+ }
366
+ const response = await getClientConfigApi(clientToUse);
367
+ configToCopy = response.servers[serverName];
368
+ }
369
+
370
+ if (!configToCopy) {
371
+ throw new Error('Server configuration not found.');
372
+ }
373
+
374
+ const text = JSON.stringify({
375
+ name: serverName,
376
+ config: configToCopy
377
+ }, null, 2);
378
+
379
+ await navigator.clipboard.writeText(text);
380
+
381
+ // Visual feedback
382
+ const btn = event ? event.target : document.activeElement;
383
+ const originalText = btn.textContent;
384
+ btn.textContent = '✓';
385
+ setTimeout(() => btn.textContent = originalText, 1000);
386
+ } catch (error) {
387
+ alert('Failed to copy to clipboard: ' + error.message);
388
+ }
389
+ }
390
+
391
+ export async function exportServer(serverName, clientId = null) {
392
+ try {
393
+ const clientToExportFrom = clientId || currentClient;
394
+ const data = await exportServerApi(clientToExportFrom, serverName);
395
+
396
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
397
+ const url = URL.createObjectURL(blob);
398
+ const a = document.createElement('a');
399
+ a.href = url;
400
+ a.download = `${currentClient}-${serverName}.json`;
401
+ a.click();
402
+ URL.revokeObjectURL(url);
403
+ } catch (error) {
404
+ alert('Failed to export server: ' + error.message);
405
+ }
406
+ }
407
+
408
+ export async function exportConfig() {
409
+ try {
410
+ const data = await exportConfigApi(currentClient);
411
+
412
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
413
+ const url = URL.createObjectURL(blob);
414
+ const a = document.createElement('a');
415
+ a.href = url;
416
+ a.download = `${currentClient}-config.json`;
417
+ a.click();
418
+ URL.revokeObjectURL(url);
419
+ } catch (error) {
420
+ alert('Failed to export config: ' + error.message);
421
+ }
422
+ }
423
+
424
+ export function showImportModal(loadClientServers, renderKanbanBoard, loadClientsFn) {
425
+ const modal = document.getElementById('importModal');
426
+ const form = document.getElementById('importForm');
427
+
428
+ modal.style.display = 'flex';
429
+ document.getElementById('importData').value = '';
430
+
431
+ form.onsubmit = async (e) => {
432
+ e.preventDefault();
433
+
434
+ try {
435
+ const importData = JSON.parse(document.getElementById('importData').value);
436
+
437
+ const response = await importConfigApi(currentClient, importData);
438
+
439
+ if (response.success) {
440
+ modal.style.display = 'none';
441
+ await loadClientServers();
442
+ loadClientsFn();
443
+ } else {
444
+ throw new Error('Failed to import configuration');
445
+ }
446
+ } catch (error) {
447
+ alert('Failed to import: ' + error.message);
448
+ }
449
+ };
450
+ }
451
+
452
+ // Bulk actions functions
453
+ export function updateBulkActions() {
454
+ const checkboxes = document.querySelectorAll('.server-checkbox:checked');
455
+ const bulkActions = document.getElementById('bulkActions');
456
+ const selectedCount = document.getElementById('selectedCount');
457
+
458
+ if (checkboxes.length > 0) {
459
+ bulkActions.style.display = 'flex';
460
+ selectedCount.textContent = `${checkboxes.length} selected`;
461
+ } else {
462
+ bulkActions.style.display = 'none';
463
+ }
464
+ }
465
+
466
+ export function selectAllServers() {
467
+ document.querySelectorAll('.server-checkbox').forEach(cb => cb.checked = true);
468
+ updateBulkActions();
469
+ }
470
+
471
+ export function deselectAllServers() {
472
+ document.querySelectorAll('.server-checkbox').forEach(cb => cb.checked = false);
473
+ updateBulkActions();
474
+ }
475
+
476
+ export async function deleteSelected(loadClientServers, renderKanbanBoard, loadClientsFn) {
477
+ const checkboxes = document.querySelectorAll('.server-checkbox:checked');
478
+ const serverNames = Array.from(checkboxes).map(cb => cb.dataset.server);
479
+
480
+ if (serverNames.length === 0) return;
481
+
482
+ if (!confirm(`Are you sure you want to delete ${serverNames.length} server(s)?`)) {
483
+ return;
484
+ }
485
+
486
+ let deletedCount = 0;
487
+ for (const serverName of serverNames) {
488
+ try {
489
+ const response = await deleteServerApi(currentClient, serverName);
490
+
491
+ if (response.success) {
492
+ deletedCount++;
493
+ }
494
+ } catch (error) {
495
+ // Silently continue with other deletions
496
+ }
497
+ }
498
+
499
+ await loadClientServers();
500
+ if (renderKanbanBoard) {
501
+ await renderKanbanBoard();
502
+ }
503
+ loadClientsFn();
504
+ }
505
+
506
+ export async function removeFromAll(serverName, loadClients, loadClientServers, loadClientsFn) {
507
+ if (!confirm(`Are you sure you want to remove "${serverName}" from ALL clients?`)) {
508
+ return;
509
+ }
510
+
511
+ let removedCount = 0;
512
+ const errors = [];
513
+
514
+ for (const client of clients) {
515
+ try {
516
+ // Check if server exists in this client
517
+ const config = await getClientConfigApi(client.id);
518
+
519
+ if (config.servers && config.servers[serverName]) {
520
+ const deleteResponse = await deleteServerApi(client.id, serverName);
521
+
522
+ if (deleteResponse.success) {
523
+ removedCount++;
524
+ } else {
525
+ errors.push(client.name);
526
+ }
527
+ }
528
+ } catch (error) {
529
+ errors.push(client.name);
530
+ console.error(`Failed to remove from ${client.id}:`, error);
531
+ }
532
+ }
533
+
534
+ if (errors.length > 0) {
535
+ alert(`Removed "${serverName}" from ${removedCount} client(s).\nFailed for: ${errors.join(', ')}`);
536
+ } else {
537
+ }
538
+
539
+ loadClientsFn();
540
+ await loadClientServers();
541
+ }
542
+
543
+ export const showAddServerToClientsModal = (serverName, serverConfig) => {
544
+ const modal = document.getElementById('addServerToClientsModal');
545
+ document.getElementById('addServerToClientsTitle').textContent = `Add Server ${serverName} to Clients`;
546
+ document.getElementById('addServerToClientsName').textContent = serverName;
547
+ document.getElementById('addServerToClientsConfig').value = JSON.stringify(serverConfig, null, 2);
548
+
549
+ const addServerToClientsListDiv = document.getElementById('addServerToClientsList');
550
+ addServerToClientsListDiv.innerHTML = '';
551
+ clients.forEach(client => {
552
+ const item = document.createElement('div');
553
+ item.className = 'checkbox-item';
554
+ item.innerHTML = `
555
+ <input type="checkbox" id="addServerClient-${client.id}" value="${client.id}" checked>
556
+ <label for="addServerClient-${client.id}">${client.name}</label>
557
+ `;
558
+ addServerToClientsListDiv.appendChild(item);
559
+ });
560
+
561
+ document.getElementById('selectAllAddServerClients').onclick = () => {
562
+ addServerToClientsListDiv.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = true);
563
+ };
564
+ document.getElementById('selectNoneAddServerClients').onclick = () => {
565
+ addServerToClientsListDiv.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
566
+ };
567
+
568
+ modal.style.display = 'flex';
569
+
570
+ document.getElementById('addServerToClientsForm').onsubmit = async (e) => {
571
+ e.preventDefault();
572
+ await addServerToSelectedClients(serverName, serverConfig);
573
+ };
574
+ };
575
+
576
+ const addServerToSelectedClients = async (serverName, serverConfig) => {
577
+ const selectedClientIds = Array.from(document.querySelectorAll('#addServerToClientsList input[type="checkbox"]:checked'))
578
+ .map(cb => cb.value);
579
+
580
+ if (selectedClientIds.length === 0) {
581
+ alert('Please select at least one client to add the server to.');
582
+ return;
583
+ }
584
+
585
+ try {
586
+ const response = await addServerToMultipleClientsApi(serverName, serverConfig, selectedClientIds);
587
+
588
+ if (response.success) {
589
+ document.getElementById('addServerToClientsModal').style.display = 'none';
590
+ loadClientsCallback(); // Re-render to show changes
591
+ } else {
592
+ throw new Error('Failed to add server to clients');
593
+ }
594
+ } catch (error) {
595
+ alert('Failed to add server to clients: ' + error.message);
596
+ }
597
+ };
598
+
599
+ export const showCopyEnvVarsModal = async (sourceServerName, sourceClientId, allClients, loadClientsFn) => {
600
+ const modal = document.getElementById('copyEnvVarsModal');
601
+ document.getElementById('copyEnvVarsSourceServer').textContent = sourceServerName;
602
+ document.getElementById('copyEnvVarsSourceClient').textContent = allClients.find(c => c.id === sourceClientId).name;
603
+
604
+ const sourceClientConfig = await getClientConfigApi(sourceClientId);
605
+ const sourceServerConfig = sourceClientConfig.servers[sourceServerName];
606
+ const envVars = sourceServerConfig.env || {};
607
+
608
+ const copyEnvVarSelect = document.getElementById('copyEnvVarSelect');
609
+ copyEnvVarSelect.innerHTML = '';
610
+ for (const key of Object.keys(envVars)) {
611
+ const option = document.createElement('option');
612
+ option.value = key;
613
+ option.textContent = key;
614
+ copyEnvVarSelect.appendChild(option);
615
+ }
616
+
617
+ const copyEnvVarsTargetClientsDiv = document.getElementById('copyEnvVarsTargetClients');
618
+ copyEnvVarsTargetClientsDiv.innerHTML = '';
619
+ clients.forEach(client => {
620
+ if (client.id !== sourceClientId) {
621
+ const item = document.createElement('div');
622
+ item.className = 'checkbox-item';
623
+ item.innerHTML = `
624
+ <input type="checkbox" id="copyEnvClient-${client.id}" value="${client.id}" checked>
625
+ <label for="copyEnvClient-${client.id}">${client.name}</label>
626
+ `;
627
+ copyEnvVarsTargetClientsDiv.appendChild(item);
628
+ }
629
+ });
630
+
631
+ document.getElementById('selectAllCopyEnvVarsClients').onclick = () => {
632
+ copyEnvVarsTargetClientsDiv.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = true);
633
+ };
634
+ document.getElementById('selectNoneCopyEnvVarsClients').onclick = () => {
635
+ copyEnvVarsTargetClientsDiv.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
636
+ };
637
+
638
+ modal.style.display = 'flex';
639
+
640
+ document.getElementById('copyEnvVarsForm').onsubmit = async (e) => {
641
+ e.preventDefault();
642
+ const selectedEnvVarKey = copyEnvVarSelect.value;
643
+ const selectedEnvVarValue = envVars[selectedEnvVarKey];
644
+ const targetClientIds = Array.from(document.querySelectorAll('#copyEnvVarsTargetClients input[type="checkbox"]:checked'))
645
+ .map(cb => cb.value);
646
+
647
+ if (!selectedEnvVarKey) {
648
+ alert('Please select an environment variable to copy.');
649
+ return;
650
+ }
651
+ if (targetClientIds.length === 0) {
652
+ alert('Please select at least one target client.');
653
+ return;
654
+ }
655
+
656
+ try {
657
+ for (const clientId of targetClientIds) {
658
+ // Assuming updateServerEnvApi can handle adding new env vars if they don't exist
659
+ await updateServerEnvApi(sourceServerName, selectedEnvVarKey, selectedEnvVarValue, [clientId]);
660
+ }
661
+ document.getElementById('copyEnvVarsModal').style.display = 'none';
662
+ if (typeof loadClientsFn === 'function') {
663
+ loadClientsFn(); // Refresh all clients to show changes
664
+ } else if (typeof loadClientsCallback === 'function') {
665
+ loadClientsCallback(); // Use global callback if loadClientsFn is not available
666
+ }
667
+ } catch (error) {
668
+ alert('Failed to copy environment variable: ' + error.message);
669
+ }
670
+ };
671
+ };
672
+
673
+ document.getElementById('cancelCopyEnvVars').onclick = () => {
674
+ document.getElementById('copyEnvVarsModal').style.display = 'none';
675
+ };
676
+
677
+ export const showCopySingleEnvVarModal = async (serverName, envKey, envValue, sourceClientId, allClients, loadClientsFn) => {
678
+ const modal = document.getElementById('copySingleEnvVarModal');
679
+ document.getElementById('copySingleEnvVarKey').textContent = envKey;
680
+ document.getElementById('copySingleEnvVarValue').textContent = envValue;
681
+
682
+ // Clear any existing event listeners by cloning the form
683
+ const oldForm = document.getElementById('copySingleEnvVarForm');
684
+ const newForm = oldForm.cloneNode(true);
685
+ oldForm.parentNode.replaceChild(newForm, oldForm);
686
+
687
+ // Get reference to the div AFTER cloning the form
688
+ const copySingleEnvVarTargetClientsDiv = document.getElementById('copySingleEnvVarTargetClients');
689
+ copySingleEnvVarTargetClientsDiv.innerHTML = '';
690
+
691
+ for (const client of allClients) {
692
+ // Only skip if sourceClientId is valid and matches current client
693
+ if (sourceClientId && client.id === sourceClientId) {
694
+ continue;
695
+ }
696
+
697
+ try {
698
+ const clientConfig = await getClientConfigApi(client.id);
699
+ if (clientConfig.servers && clientConfig.servers[serverName]) {
700
+ const item = document.createElement('div');
701
+ item.className = 'checkbox-item';
702
+ item.innerHTML = `
703
+ <input type="checkbox" id="copySingleEnvClient-${client.id}" value="${client.id}" checked>
704
+ <label for="copySingleEnvClient-${client.id}">${client.name}</label>
705
+ `;
706
+ copySingleEnvVarTargetClientsDiv.appendChild(item);
707
+ }
708
+ } catch (error) {
709
+ console.warn(`Could not read config for client ${client.id}:`, error);
710
+ // Skip clients that cannot be read
711
+ }
712
+ }
713
+
714
+ // Re-attach event handlers after form cloning
715
+ document.getElementById('selectAllCopySingleEnvVarClients').onclick = () => {
716
+ copySingleEnvVarTargetClientsDiv.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = true);
717
+ };
718
+ document.getElementById('selectNoneCopySingleEnvVarClients').onclick = () => {
719
+ copySingleEnvVarTargetClientsDiv.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
720
+ };
721
+
722
+ modal.style.display = 'flex';
723
+
724
+ // Set up cancel button handler when modal is shown
725
+ document.getElementById('cancelCopySingleEnvVar').onclick = () => {
726
+ modal.style.display = 'none';
727
+ };
728
+
729
+ // Re-attach form submission handler after cloning
730
+ document.getElementById('copySingleEnvVarForm').onsubmit = async (e) => {
731
+ e.preventDefault();
732
+ const targetClientIds = Array.from(document.querySelectorAll('#copySingleEnvVarTargetClients input[type="checkbox"]:checked'))
733
+ .map(cb => cb.value);
734
+
735
+ if (targetClientIds.length === 0) {
736
+ alert('Please select at least one target client.');
737
+ return;
738
+ }
739
+
740
+ try {
741
+ for (const clientId of targetClientIds) {
742
+ await updateServerEnvApi(serverName, envKey, envValue, [clientId]);
743
+ }
744
+ document.getElementById('copySingleEnvVarModal').style.display = 'none';
745
+ if (typeof loadClientsFn === 'function') {
746
+ loadClientsFn(); // Refresh all clients to show changes
747
+ } else if (typeof loadClientsCallback === 'function') {
748
+ loadClientsCallback(); // Use global callback if loadClientsFn is not available
749
+ }
750
+ } catch (error) {
751
+ alert('Failed to copy environment variable: ' + error.message);
752
+ }
753
+ };
754
+ };
755
+
756
+
757
+ export function showRemoteServerModal(loadClientServers, renderKanbanBoard, clientId = null, loadClientsFn) {
758
+ const modal = document.getElementById('remoteServerModal');
759
+
760
+ // Store the client ID in the modal's data attribute
761
+ if (clientId) {
762
+ currentClient = clientId;
763
+ modal.dataset.clientId = clientId;
764
+ } else if (!currentClient && modal.dataset.clientId) {
765
+ currentClient = modal.dataset.clientId;
766
+ }
767
+
768
+ // Clear the form
769
+ document.getElementById('remoteServerName').value = '';
770
+ document.getElementById('remoteServerUrl').value = '';
771
+
772
+ modal.style.display = 'flex';
773
+ document.getElementById('remoteServerName').focus();
774
+
775
+ const form = document.getElementById('remoteServerForm');
776
+ form.onsubmit = async (e) => {
777
+ e.preventDefault();
778
+
779
+ const modalClient = modal.dataset.clientId || currentClient;
780
+ if (!modalClient) {
781
+ alert('No client selected. Please close this modal and select a client first.');
782
+ return;
783
+ }
784
+
785
+ const serverName = document.getElementById('remoteServerName').value.trim();
786
+ const serverUrl = document.getElementById('remoteServerUrl').value.trim();
787
+
788
+ if (!serverName || !serverUrl) {
789
+ alert('Please provide both server name and URL.');
790
+ return;
791
+ }
792
+
793
+ // Create the configuration for remote MCP server
794
+ const serverConfig = {
795
+ command: "npx",
796
+ args: ["mcp-remote", serverUrl]
797
+ };
798
+
799
+ try {
800
+ await addServerApi(modalClient, serverName, serverConfig);
801
+ modal.style.display = 'none';
802
+
803
+ // Reload the UI
804
+ if (loadClientServers) {
805
+ loadClientServers(modalClient);
806
+ }
807
+ if (renderKanbanBoard) {
808
+ renderKanbanBoard();
809
+ }
810
+ if (loadClientsFn) {
811
+ loadClientsFn();
812
+ }
813
+ } catch (error) {
814
+ alert('Failed to add remote MCP server: ' + error.message);
815
+ }
816
+ };
817
+
818
+ // Cancel button
819
+ document.getElementById('cancelRemoteServer').onclick = () => {
820
+ modal.style.display = 'none';
821
+ };
822
+ }