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,299 @@
1
+ import { getAllServersApi, updateServerEnvApi, addServerToMultipleClientsApi } from './api.js';
2
+ import { addEnvVarRow } from './utils.js';
3
+ import { showServerModal, deleteServer, copyToClipboard, exportServer, removeFromAll } from './modals.js'; // Import from modals.js
4
+
5
+ let clients = []; // This will be passed from main.js
6
+ let loadClientsCallback = null; // Callback to main.js to reload all clients
7
+
8
+ export function initServerView(allClients, loadClientsFn) {
9
+ clients = allClients;
10
+ loadClientsCallback = loadClientsFn;
11
+ }
12
+
13
+ export async function renderAllServers() {
14
+ const allServersList = document.getElementById('allServersList');
15
+ allServersList.innerHTML = '<p>Loading servers...</p>';
16
+ try {
17
+ const allServers = await getAllServersApi();
18
+ allServersList.innerHTML = ''; // Clear loading message
19
+
20
+ if (Object.keys(allServers).length === 0) {
21
+ allServersList.innerHTML = '<p style="color: #7f8c8d;">No servers found across all clients.</p>';
22
+ return;
23
+ }
24
+
25
+ for (const [serverName, serverData] of Object.entries(allServers)) {
26
+ const card = document.createElement('div');
27
+ card.className = 'server-card-all';
28
+
29
+ let clientsHtml = '';
30
+ if (serverData.clients && serverData.clients.length > 0) {
31
+ clientsHtml = '<div class="server-clients"><strong>Clients:</strong> ';
32
+ clientsHtml += serverData.clients.map(c => c.name).join(', ');
33
+ clientsHtml += '</div>';
34
+ }
35
+
36
+ let envHtml = '';
37
+ if (serverData.config && serverData.config.env && Object.keys(serverData.config.env).length > 0) {
38
+ envHtml = '<div class="env-list"><strong>Environment:</strong><br>';
39
+ for (const [key, value] of Object.entries(serverData.config.env)) {
40
+ const displayValue = value.includes('KEY') || value.includes('SECRET')
41
+ ? value.substring(0, 4) + '***'
42
+ : value;
43
+ envHtml += `${key}: ${displayValue}<br>`;
44
+ }
45
+ envHtml += '</div>';
46
+ }
47
+
48
+ card.innerHTML = `
49
+ <div class="server-header-all">
50
+ <span class="server-name-all">${serverName}</span>
51
+ ${serverData.global ? '<span class="global-tag">Global</span>' : ''}
52
+ <div class="server-actions-all">
53
+ <button class="btn btn-small btn-secondary edit-server-full-btn" data-server-name="${serverName}" data-clients='${JSON.stringify(serverData.clients)}' data-server-config='${JSON.stringify(serverData.config)}'>Edit</button>
54
+ <button class="btn btn-small btn-primary add-to-clients-btn" data-server-name="${serverName}" data-server-config='${JSON.stringify(serverData.config)}'>Add to More Clients</button>
55
+ <button class="btn btn-small btn-secondary copy-to-clipboard-server-view-btn" data-server-name="${serverName}" data-server-config='${JSON.stringify(serverData.config)}'>📋</button>
56
+ <button class="btn btn-small btn-secondary export-server-view-btn" data-server-name="${serverName}">💾</button>
57
+ <button class="btn btn-small btn-danger delete-server-view-btn" data-server-name="${serverName}">Delete</button>
58
+ </div>
59
+ </div>
60
+ <div class="server-details-all">
61
+ ${serverData.config && serverData.config.command ? `<div class="detail-row"><strong>Command:</strong> ${serverData.config.command}</div>` : ''}
62
+ ${serverData.config && serverData.config.args ? `<div class="detail-row"><strong>Args:</strong> ${serverData.config.args.join(' ')}</div>` : ''}
63
+ ${envHtml}
64
+ ${clientsHtml}
65
+ </div>
66
+ `;
67
+ allServersList.appendChild(card);
68
+ }
69
+ attachServerViewEventListeners();
70
+ } catch (error) {
71
+ console.error('Failed to load all servers:', error);
72
+ allServersList.innerHTML = '<p style="color: #e74c3c;">Error loading servers.</p>';
73
+ }
74
+ }
75
+
76
+ function attachServerViewEventListeners() {
77
+ document.querySelectorAll('.edit-server-full-btn').forEach(button => {
78
+ button.addEventListener('click', (e) => {
79
+ const serverName = e.currentTarget.dataset.serverName;
80
+ const serverConfig = JSON.parse(e.currentTarget.dataset.serverConfig);
81
+ const clients = JSON.parse(e.currentTarget.dataset.clients);
82
+ const clientIds = clients.map(c => c.id);
83
+ showServerModal(serverName, serverConfig, renderAllServers, null, clientIds, loadClientsCallback);
84
+ });
85
+ });
86
+
87
+ document.querySelectorAll('.add-to-clients-btn').forEach(button => {
88
+ button.addEventListener('click', (e) => {
89
+ const serverName = e.target.dataset.serverName;
90
+ const serverConfig = JSON.parse(e.target.dataset.serverConfig);
91
+ showAddServerToClientsModal(serverName, serverConfig);
92
+ });
93
+ });
94
+
95
+ document.querySelectorAll('.copy-to-clipboard-server-view-btn').forEach(button => {
96
+ button.addEventListener('click', (e) => {
97
+ const serverName = e.target.dataset.serverName;
98
+ const serverConfig = JSON.parse(e.target.dataset.serverConfig);
99
+ copyToClipboard(serverName, e, serverConfig); // Pass serverConfig
100
+ });
101
+ });
102
+
103
+ document.querySelectorAll('.export-server-view-btn').forEach(button => {
104
+ button.addEventListener('click', (e) => {
105
+ const serverName = e.target.dataset.serverName;
106
+ exportServer(serverName);
107
+ });
108
+ });
109
+
110
+ document.querySelectorAll('.delete-server-view-btn').forEach(button => {
111
+ button.addEventListener('click', (e) => {
112
+ const serverName = e.target.dataset.serverName;
113
+ if (!confirm(`Are you sure you want to delete the server "${serverName}" from ALL clients?`)) {
114
+ return;
115
+ }
116
+ removeFromAll(serverName, null, renderAllServers, loadClientsCallback); // Pass renderAllServers to re-render
117
+ });
118
+ });
119
+ }
120
+
121
+ export const showEditServerEnvModal = (serverName, currentEnv) => {
122
+ const modal = document.getElementById('editServerEnvModal');
123
+ document.getElementById('editServerEnvTitle').textContent = `Edit Environment Variable for Server ${serverName}`;
124
+ document.getElementById('editServerEnvName').textContent = serverName;
125
+
126
+ const editEnvVarsDiv = document.getElementById('editEnvVars');
127
+ editEnvVarsDiv.innerHTML = `
128
+ <div class="form-group">
129
+ <label for="envVarKeySelect">Environment Variable Key</label>
130
+ <select id="envVarKeySelect">
131
+ <option value="">-- Select or type new --</option>
132
+ ${Object.keys(currentEnv).map(key => `<option value="${key}">${key}</option>`).join('')}
133
+ </select>
134
+ <input type="text" id="newEnvVarKeyInput" placeholder="Type new key if not in list" style="margin-top: 5px;">
135
+ </div>
136
+ <div class="form-group">
137
+ <label for="envVarValueInput">Environment Variable Value</label>
138
+ <input type="text" id="envVarValueInput">
139
+ </div>
140
+ `;
141
+
142
+ // Pre-fill value if an existing key is selected
143
+ document.getElementById('envVarKeySelect').onchange = (e) => {
144
+ const selectedKey = e.target.value;
145
+ document.getElementById('newEnvVarKeyInput').value = selectedKey;
146
+ document.getElementById('envVarValueInput').value = currentEnv[selectedKey] || '';
147
+ };
148
+
149
+ // Sync new key input with select
150
+ document.getElementById('newEnvVarKeyInput').oninput = (e) => {
151
+ const inputVal = e.target.value;
152
+ const select = document.getElementById('envVarKeySelect');
153
+ let found = false;
154
+ for (let i = 0; i < select.options.length; i++) {
155
+ if (select.options[i].value === inputVal) {
156
+ select.value = inputVal;
157
+ found = true;
158
+ break;
159
+ }
160
+ }
161
+ if (!found) {
162
+ select.value = ""; // Reset select if input doesn't match an option
163
+ }
164
+ };
165
+
166
+ const editEnvClientsDiv = document.getElementById('editEnvClients');
167
+ editEnvClientsDiv.innerHTML = '';
168
+ clients.forEach(client => {
169
+ const item = document.createElement('div');
170
+ item.className = 'checkbox-item';
171
+ item.innerHTML = `
172
+ <input type="checkbox" id="editEnvClient-${client.id}" value="${client.id}" checked>
173
+ <label for="editEnvClient-${client.id}">${client.name}</label>
174
+ `;
175
+ editEnvClientsDiv.appendChild(item);
176
+ });
177
+
178
+ document.getElementById('selectAllEditEnvClients').onclick = () => {
179
+ editEnvClientsDiv.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = true);
180
+ };
181
+ document.getElementById('selectNoneEditEnvClients').onclick = () => {
182
+ editEnvClientsDiv.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
183
+ };
184
+
185
+ // Add 'Apply to all servers' checkbox
186
+ const applyToAllServersDiv = document.createElement('div');
187
+ applyToAllServersDiv.className = 'form-group checkbox-item';
188
+ applyToAllServersDiv.innerHTML = `
189
+ <input type="checkbox" id="applyToAllServers" value="true">
190
+ <label for="applyToAllServers">Apply to all servers in selected clients</label>
191
+ `;
192
+ editEnvClientsDiv.parentNode.insertBefore(applyToAllServersDiv, editEnvClientsDiv.nextSibling);
193
+
194
+ modal.style.display = 'flex';
195
+
196
+ document.getElementById('editServerEnvForm').onsubmit = async (e) => {
197
+ e.preventDefault();
198
+ const envKey = document.getElementById('newEnvVarKeyInput').value;
199
+ const envValue = document.getElementById('envVarValueInput').value;
200
+ const applyToAll = document.getElementById('applyToAllServers').checked;
201
+ await saveEditedServerEnv(serverName, envKey, envValue, applyToAll);
202
+ };
203
+ };
204
+
205
+ const saveEditedServerEnv = async (serverName, envKey, envValue, applyToAll) => {
206
+ if (!envKey) {
207
+ alert('Environment variable key cannot be empty.');
208
+ return;
209
+ }
210
+
211
+ const selectedClientIds = Array.from(document.querySelectorAll('#editEnvClients input[type="checkbox"]:checked'))
212
+ .map(cb => cb.value);
213
+
214
+ if (selectedClientIds.length === 0) {
215
+ alert('Please select at least one client to apply changes.');
216
+ return;
217
+ }
218
+
219
+ try {
220
+ if (applyToAll) {
221
+ // Apply to all servers in selected clients
222
+ for (const clientId of selectedClientIds) {
223
+ const clientConfig = await getClientConfigApi(clientId);
224
+ for (const sName of Object.keys(clientConfig.servers)) {
225
+ await updateServerEnvApi(sName, envKey, envValue, [clientId]);
226
+ }
227
+ }
228
+ } else {
229
+ // Apply only to the specific server in selected clients
230
+ const response = await updateServerEnvApi(serverName, envKey, envValue, selectedClientIds);
231
+ if (!response.success) {
232
+ throw new Error(`Failed to update ${envKey}`);
233
+ }
234
+ }
235
+
236
+ document.getElementById('editServerEnvModal').style.display = 'none';
237
+ alert('Environment variable updated successfully!');
238
+ renderAllServers(); // Re-render to show changes
239
+ } catch (error) {
240
+ alert('Failed to save environment variable: ' + error.message);
241
+ }
242
+ };
243
+
244
+ export const showAddServerToClientsModal = (serverName, serverConfig) => {
245
+ const modal = document.getElementById('addServerToClientsModal');
246
+ document.getElementById('addServerToClientsTitle').textContent = `Add Server ${serverName} to Clients`;
247
+ document.getElementById('addServerToClientsName').textContent = serverName;
248
+ document.getElementById('addServerToClientsConfig').value = JSON.stringify(serverConfig, null, 2);
249
+
250
+ const addServerToClientsListDiv = document.getElementById('addServerToClientsList');
251
+ addServerToClientsListDiv.innerHTML = '';
252
+ clients.forEach(client => {
253
+ const item = document.createElement('div');
254
+ item.className = 'checkbox-item';
255
+ item.innerHTML = `
256
+ <input type="checkbox" id="addServerClient-${client.id}" value="${client.id}" checked>
257
+ <label for="addServerClient-${client.id}">${client.name}</label>
258
+ `;
259
+ addServerToClientsListDiv.appendChild(item);
260
+ });
261
+
262
+ document.getElementById('selectAllAddServerClients').onclick = () => {
263
+ addServerToClientsListDiv.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = true);
264
+ };
265
+ document.getElementById('selectNoneAddServerClients').onclick = () => {
266
+ addServerToClientsListDiv.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
267
+ };
268
+
269
+ modal.style.display = 'flex';
270
+
271
+ document.getElementById('addServerToClientsForm').onsubmit = async (e) => {
272
+ e.preventDefault();
273
+ await addServerToSelectedClients(serverName, serverConfig);
274
+ };
275
+ };
276
+
277
+ const addServerToSelectedClients = async (serverName, serverConfig) => {
278
+ const selectedClientIds = Array.from(document.querySelectorAll('#addServerToClientsList input[type="checkbox"]:checked'))
279
+ .map(cb => cb.value);
280
+
281
+ if (selectedClientIds.length === 0) {
282
+ alert('Please select at least one client to add the server to.');
283
+ return;
284
+ }
285
+
286
+ try {
287
+ const response = await addServerToMultipleClientsApi(serverName, serverConfig, selectedClientIds);
288
+
289
+ if (response.success) {
290
+ document.getElementById('addServerToClientsModal').style.display = 'none';
291
+ alert(`Server ${serverName} added to selected clients successfully!`);
292
+ renderAllServers(); // Re-render to show changes
293
+ } else {
294
+ throw new Error('Failed to add server to clients');
295
+ }
296
+ } catch (error) {
297
+ alert('Failed to add server to clients: ' + error.message);
298
+ }
299
+ };
@@ -0,0 +1,185 @@
1
+ export function addEnvVarRow(envVarsDiv, key = '', value = '') {
2
+ const row = document.createElement('div');
3
+ row.className = 'env-var-row';
4
+
5
+ row.innerHTML = `
6
+ <input type="text" placeholder="Key" value="${key}" class="env-key">
7
+ <input type="text" placeholder="Value" value="${value}" class="env-value">
8
+ <button type="button" class="icon-btn secondary copy-env-var-btn" data-key="${key}" data-value="${value}" title="Copy">➡️</button>
9
+ <button type="button" class="icon-btn danger" onclick="this.parentElement.remove()" title="Remove">🗑️</button>
10
+ `;
11
+
12
+ envVarsDiv.appendChild(row);
13
+ }
14
+
15
+ export function switchTab(tab) {
16
+ document.querySelectorAll('.tab-btn').forEach(btn => {
17
+ btn.classList.toggle('active', btn.dataset.tab === tab);
18
+ });
19
+
20
+ document.getElementById('formTab').style.display = tab === 'form' ? 'block' : 'none';
21
+ document.getElementById('jsonTab').style.display = tab === 'json' ? 'block' : 'none';
22
+
23
+ // Disable/enable form validation based on active tab
24
+ const formControls = document.querySelectorAll('#formTab input, #formTab textarea');
25
+ const jsonControls = document.querySelectorAll('#jsonTab textarea');
26
+
27
+ if (tab === 'json') {
28
+ // Disable validation on form tab controls when JSON tab is active
29
+ formControls.forEach(control => {
30
+ control.setAttribute('data-original-required', control.hasAttribute('required'));
31
+ control.removeAttribute('required');
32
+ });
33
+ // Ensure JSON editor doesn't have required attribute either
34
+ jsonControls.forEach(control => {
35
+ control.removeAttribute('required');
36
+ });
37
+ } else {
38
+ // Restore validation on form tab controls when form tab is active
39
+ formControls.forEach(control => {
40
+ if (control.getAttribute('data-original-required') === 'true') {
41
+ control.setAttribute('required', '');
42
+ }
43
+ control.removeAttribute('data-original-required');
44
+ });
45
+ // Disable validation on JSON tab controls
46
+ jsonControls.forEach(control => {
47
+ control.removeAttribute('required');
48
+ });
49
+ }
50
+
51
+ // Sync data between tabs
52
+ if (tab === 'json') {
53
+ // When switching to JSON tab, update only the fields that form controls
54
+ try {
55
+ const existingJson = JSON.parse(document.getElementById('jsonEditor').value);
56
+ const formConfig = buildConfigFromForm();
57
+
58
+ // Merge form data into existing JSON, preserving other properties
59
+ const mergedConfig = { ...existingJson };
60
+
61
+ // Update command - if empty in form, remove from JSON
62
+ if (formConfig.command) {
63
+ mergedConfig.command = formConfig.command;
64
+ } else if (document.getElementById('serverCommand').value === '') {
65
+ delete mergedConfig.command;
66
+ }
67
+
68
+ // Update args - if empty in form, remove from JSON
69
+ if (formConfig.args && formConfig.args.length > 0) {
70
+ mergedConfig.args = formConfig.args;
71
+ } else if (document.getElementById('serverArgs').value === '') {
72
+ delete mergedConfig.args;
73
+ }
74
+
75
+ // For env vars, only update/remove the ones that were shown in form
76
+ // Preserve any env vars that weren't displayed in the form
77
+ if (mergedConfig.env || formConfig.env) {
78
+ // Start with ALL existing env vars (including ones not shown in form)
79
+ const updatedEnv = { ...(mergedConfig.env || {}) };
80
+
81
+ // Get the keys that were initially loaded into the form
82
+ const initialFormKeys = document.getElementById('envVars').dataset.initialKeys;
83
+ const initialKeys = initialFormKeys ? JSON.parse(initialFormKeys) : [];
84
+
85
+ // Create a set of initial keys for quick lookup
86
+ const initialKeysSet = new Set(initialKeys);
87
+
88
+ // Get the current state of form env vars
89
+ const currentFormEnvs = {};
90
+ const currentFormKeys = new Set();
91
+ document.querySelectorAll('.env-var-row').forEach(row => {
92
+ const key = row.querySelector('.env-key').value;
93
+ const value = row.querySelector('.env-value').value;
94
+ if (key) {
95
+ currentFormKeys.add(key);
96
+ currentFormEnvs[key] = value;
97
+ }
98
+ });
99
+
100
+ // Update or remove only the env vars that were initially shown in the form
101
+ initialKeys.forEach(key => {
102
+ if (currentFormKeys.has(key)) {
103
+ // Key still exists in form, update its value
104
+ updatedEnv[key] = currentFormEnvs[key];
105
+ } else {
106
+ // Key was removed from form, delete it
107
+ delete updatedEnv[key];
108
+ }
109
+ });
110
+
111
+ // Add any NEW env vars that were added in the form
112
+ currentFormKeys.forEach(key => {
113
+ if (!initialKeysSet.has(key)) {
114
+ // This is a new key added in the form
115
+ updatedEnv[key] = currentFormEnvs[key];
116
+ }
117
+ });
118
+
119
+ if (Object.keys(updatedEnv).length > 0) {
120
+ mergedConfig.env = updatedEnv;
121
+ } else {
122
+ delete mergedConfig.env;
123
+ }
124
+ }
125
+
126
+ document.getElementById('jsonEditor').value = JSON.stringify(mergedConfig, null, 2);
127
+ } catch (e) {
128
+ // If existing JSON is invalid, just use form data
129
+ const config = buildConfigFromForm();
130
+ document.getElementById('jsonEditor').value = JSON.stringify(config, null, 2);
131
+ }
132
+ } else {
133
+ // When switching to form tab, only update form with supported fields
134
+ try {
135
+ const config = JSON.parse(document.getElementById('jsonEditor').value);
136
+ updateFormFromConfig(config);
137
+ } catch (e) {
138
+ // Invalid JSON, don't update form
139
+ }
140
+ }
141
+ }
142
+
143
+ export function buildConfigFromForm() {
144
+ const config = {};
145
+ const command = document.getElementById('serverCommand').value;
146
+ const argsText = document.getElementById('serverArgs').value;
147
+ const args = argsText ? argsText.split('\n').filter(a => a.trim()) : [];
148
+
149
+ if (command) config.command = command;
150
+ if (args.length > 0) config.args = args;
151
+
152
+ const envVarRows = document.querySelectorAll('.env-var-row');
153
+ const env = {};
154
+ envVarRows.forEach(row => {
155
+ const key = row.querySelector('.env-key').value;
156
+ const value = row.querySelector('.env-value').value;
157
+ if (key && value) {
158
+ env[key] = value;
159
+ }
160
+ });
161
+ if (Object.keys(env).length > 0) config.env = env;
162
+
163
+ return config;
164
+ }
165
+
166
+ export function updateFormFromConfig(config) {
167
+ // Only sync the supported fields to the form, ignore any other JSON properties
168
+ document.getElementById('serverCommand').value = config.command || '';
169
+ document.getElementById('serverArgs').value = config.args?.join('\n') || '';
170
+
171
+ const envVarsDiv = document.getElementById('envVars');
172
+ envVarsDiv.innerHTML = '';
173
+
174
+ // Track which env keys we're loading into the form
175
+ const loadedEnvKeys = [];
176
+ if (config.env && typeof config.env === 'object') {
177
+ for (const [key, value] of Object.entries(config.env)) {
178
+ addEnvVarRow(envVarsDiv, key, value);
179
+ loadedEnvKeys.push(key);
180
+ }
181
+ }
182
+
183
+ // Store the initially loaded keys so we know what to update later
184
+ envVarsDiv.dataset.initialKeys = JSON.stringify(loadedEnvKeys);
185
+ }