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.
- package/README.md +235 -0
- package/docs/images/kanban-view.png +0 -0
- package/docs/images/list-view.png +0 -0
- package/docs/images/remote-mcp-modal.png +0 -0
- package/docs/images/server-view.png +0 -0
- package/package.json +52 -0
- package/public/index.html +325 -0
- package/public/js/api.js +108 -0
- package/public/js/clientView.js +211 -0
- package/public/js/kanbanView.js +224 -0
- package/public/js/main.js +126 -0
- package/public/js/modals.js +822 -0
- package/public/js/serverView.js +299 -0
- package/public/js/utils.js +185 -0
- package/public/style.css +640 -0
- package/src/cli.js +394 -0
- package/src/clients.js +113 -0
- package/src/config-manager.js +520 -0
- package/src/server.js +214 -0
|
@@ -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
|
+
}
|