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
package/public/js/api.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export async function detectClientsApi() {
|
|
2
|
+
const response = await fetch('/api/clients/detect');
|
|
3
|
+
return response.json();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export async function listClientsApi() {
|
|
7
|
+
const response = await fetch('/api/clients');
|
|
8
|
+
return response.json();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function getAllServersApi() {
|
|
12
|
+
const response = await fetch('/api/servers');
|
|
13
|
+
return response.json();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function getClientConfigApi(client) {
|
|
17
|
+
const response = await fetch(`/api/clients/${client}`);
|
|
18
|
+
return response.json();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function addServerApi(client, server, config) {
|
|
22
|
+
const response = await fetch(`/api/clients/${client}/servers/${server}`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25
|
+
body: JSON.stringify(config),
|
|
26
|
+
});
|
|
27
|
+
return response.json();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function addServerToMultipleClientsApi(serverName, serverConfig, clientIds) {
|
|
31
|
+
const response = await fetch('/api/servers/add-to-clients', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ serverName, serverConfig, clientIds }),
|
|
35
|
+
});
|
|
36
|
+
return response.json();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function deleteServerApi(client, server) {
|
|
40
|
+
const response = await fetch(`/api/clients/${client}/servers/${server}`, {
|
|
41
|
+
method: 'DELETE',
|
|
42
|
+
});
|
|
43
|
+
return response.json();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function updateServerApi(client, server, config) {
|
|
47
|
+
const response = await fetch(`/api/clients/${client}/servers/${server}`, {
|
|
48
|
+
method: 'PUT',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: JSON.stringify(config),
|
|
51
|
+
});
|
|
52
|
+
return response.json();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function updateServerInClientsApi(serverName, serverConfig, clientIds) {
|
|
56
|
+
const response = await fetch(`/api/servers/${serverName}/update-in-clients`, {
|
|
57
|
+
method: 'PUT',
|
|
58
|
+
headers: { 'Content-Type': 'application/json' },
|
|
59
|
+
body: JSON.stringify({ serverConfig, clientIds }),
|
|
60
|
+
});
|
|
61
|
+
return response.json();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function updateServerEnvApi(serverName, envKey, envValue, clientIds) {
|
|
65
|
+
const response = await fetch(`/api/servers/${serverName}/env`, {
|
|
66
|
+
method: 'PUT',
|
|
67
|
+
headers: { 'Content-Type': 'application/json' },
|
|
68
|
+
body: JSON.stringify({ envKey, envValue, clientIds }),
|
|
69
|
+
});
|
|
70
|
+
return response.json();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function renameServerApi(oldName, newName) {
|
|
74
|
+
const response = await fetch(`/api/servers/${oldName}/rename`, {
|
|
75
|
+
method: 'PUT',
|
|
76
|
+
headers: { 'Content-Type': 'application/json' },
|
|
77
|
+
body: JSON.stringify({ newName }),
|
|
78
|
+
});
|
|
79
|
+
return response.json();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function copyServerApi(fromClient, fromServer, toClient, toServer) {
|
|
83
|
+
const response = await fetch('/api/copy', {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({ fromClient, fromServer, toClient, toServer }),
|
|
87
|
+
});
|
|
88
|
+
return response.json();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function exportConfigApi(client) {
|
|
92
|
+
const response = await fetch(`/api/export/${client}`);
|
|
93
|
+
return response.json();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function exportServerApi(client, server) {
|
|
97
|
+
const response = await fetch(`/api/export/${client}?server=${server}`);
|
|
98
|
+
return response.json();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function importConfigApi(client, data) {
|
|
102
|
+
const response = await fetch(`/api/import/${client}`, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: { 'Content-Type': 'application/json' },
|
|
105
|
+
body: JSON.stringify(data),
|
|
106
|
+
});
|
|
107
|
+
return response.json();
|
|
108
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { listClientsApi, getClientConfigApi, deleteServerApi } from './api.js';
|
|
2
|
+
import { showServerModal, copyServer, copyToClipboard, exportServer, deleteServer, removeFromAll, updateBulkActions, selectAllServers, deselectAllServers, deleteSelected } from './modals.js';
|
|
3
|
+
|
|
4
|
+
let currentClient = null;
|
|
5
|
+
let clients = [];
|
|
6
|
+
let loadClientsCallback = null; // Callback to main.js to reload all clients
|
|
7
|
+
|
|
8
|
+
export function initClientView(allClients, currentClientId, loadClientsFn) {
|
|
9
|
+
clients = allClients;
|
|
10
|
+
currentClient = currentClientId;
|
|
11
|
+
loadClientsCallback = loadClientsFn;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function renderClientList() {
|
|
15
|
+
const clientList = document.getElementById('clientList');
|
|
16
|
+
clientList.innerHTML = '';
|
|
17
|
+
|
|
18
|
+
const clientSort = document.getElementById('clientSort');
|
|
19
|
+
let sortBy = localStorage.getItem('clientSortBy') || 'name-asc';
|
|
20
|
+
if (clientSort) {
|
|
21
|
+
clientSort.value = sortBy;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sortedClients = [...clients].sort((a, b) => {
|
|
25
|
+
if (sortBy === 'name-asc') {
|
|
26
|
+
return a.name.localeCompare(b.name);
|
|
27
|
+
} else if (sortBy === 'name-desc') {
|
|
28
|
+
return b.name.localeCompare(a.name);
|
|
29
|
+
} else if (sortBy === 'servers-asc') {
|
|
30
|
+
return (a.serverCount || 0) - (b.serverCount || 0);
|
|
31
|
+
} else if (sortBy === 'servers-desc') {
|
|
32
|
+
return (b.serverCount || 0) - (a.serverCount || 0);
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
sortedClients.forEach(client => {
|
|
38
|
+
const item = document.createElement('div');
|
|
39
|
+
item.className = 'client-item';
|
|
40
|
+
if (client.id === currentClient) {
|
|
41
|
+
item.classList.add('active');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
item.innerHTML = `
|
|
45
|
+
<div class="client-name">${client.name}</div>
|
|
46
|
+
<div class="client-status">
|
|
47
|
+
${client.exists ? `${client.serverCount} server(s)` : 'No config'}
|
|
48
|
+
</div>
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
item.addEventListener('click', () => selectClient(client.id));
|
|
52
|
+
clientList.appendChild(item);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (clientSort) {
|
|
56
|
+
clientSort.onchange = () => {
|
|
57
|
+
localStorage.setItem('clientSortBy', clientSort.value);
|
|
58
|
+
renderClientList();
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function selectClient(clientId) {
|
|
64
|
+
currentClient = clientId;
|
|
65
|
+
// Update the global currentClient in main.js
|
|
66
|
+
if (window.setCurrentClient) {
|
|
67
|
+
window.setCurrentClient(clientId);
|
|
68
|
+
}
|
|
69
|
+
renderClientList();
|
|
70
|
+
|
|
71
|
+
const welcomeView = document.getElementById('welcomeView');
|
|
72
|
+
const clientView = document.getElementById('clientView');
|
|
73
|
+
|
|
74
|
+
welcomeView.style.display = 'none';
|
|
75
|
+
clientView.style.display = 'block';
|
|
76
|
+
|
|
77
|
+
const client = clients.find(c => c.id === clientId);
|
|
78
|
+
document.getElementById('clientName').textContent = client.name;
|
|
79
|
+
document.getElementById('configPath').textContent = client.configPath;
|
|
80
|
+
|
|
81
|
+
await loadClientServers();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function loadClientServers() {
|
|
85
|
+
try {
|
|
86
|
+
const config = await getClientConfigApi(currentClient);
|
|
87
|
+
renderServerList(config.servers || {});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Failed to load servers:', error);
|
|
90
|
+
renderServerList({});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function renderServerList(servers) {
|
|
95
|
+
const serverList = document.getElementById('serverList');
|
|
96
|
+
|
|
97
|
+
if (Object.keys(servers).length === 0) {
|
|
98
|
+
serverList.innerHTML = '<p style="color: #7f8c8d;">No servers configured</p>';
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add bulk actions toolbar
|
|
103
|
+
serverList.innerHTML = `
|
|
104
|
+
<div id="bulkActions" class="bulk-actions" style="display: none;">
|
|
105
|
+
<span id="selectedCount">0 selected</span>
|
|
106
|
+
<button class="btn btn-small btn-danger" id="deleteSelectedBtn">Delete Selected</button>
|
|
107
|
+
<button class="btn btn-small btn-secondary" id="selectAllServersBtn">Select All</button>
|
|
108
|
+
<button class="btn btn-small btn-secondary" id="deselectAllServersBtn">Deselect All</button>
|
|
109
|
+
</div>
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
113
|
+
const card = document.createElement('div');
|
|
114
|
+
card.className = 'server-card';
|
|
115
|
+
card.dataset.serverName = name;
|
|
116
|
+
|
|
117
|
+
let envHtml = '';
|
|
118
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
119
|
+
envHtml = '<div class="env-list"><strong>Environment:</strong><br>';
|
|
120
|
+
for (const [key, value] of Object.entries(server.env)) {
|
|
121
|
+
const displayValue = value.includes('KEY') || value.includes('SECRET')
|
|
122
|
+
? value.substring(0, 4) + '***'
|
|
123
|
+
: value;
|
|
124
|
+
envHtml += `${key}: ${displayValue}<br>`;
|
|
125
|
+
}
|
|
126
|
+
envHtml += '</div>';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
card.innerHTML = `
|
|
130
|
+
<div class="server-header">
|
|
131
|
+
<input type="checkbox" class="server-checkbox" data-server="${name}">
|
|
132
|
+
<span class="server-name">${name}</span>
|
|
133
|
+
<div class="server-actions">
|
|
134
|
+
<button class="btn btn-small btn-secondary edit-server-btn" data-server-name="${name}">Edit</button>
|
|
135
|
+
<button class="btn btn-small btn-secondary export-server-btn" data-server-name="${name}">Export</button>
|
|
136
|
+
<button class="btn btn-small btn-danger delete-server-btn" data-server-name="${name}">Delete</button>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="server-details">
|
|
140
|
+
${server.command ? `<div class="detail-row"><strong>Command:</strong> ${server.command}</div>` : ''}
|
|
141
|
+
${server.args ? `<div class="detail-row"><strong>Args:</strong> ${server.args.join(' ')}</div>` : ''}
|
|
142
|
+
${envHtml}
|
|
143
|
+
<div class="detail-row"><button class="icon-btn secondary copy-to-clipboard-btn" data-server-name="${name}" title="Copy to Clipboard">📋</button></div>
|
|
144
|
+
</div>
|
|
145
|
+
`;
|
|
146
|
+
|
|
147
|
+
serverList.appendChild(card);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
attachClientViewEventListeners();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function attachClientViewEventListeners() {
|
|
154
|
+
document.querySelectorAll('.server-checkbox').forEach(checkbox => {
|
|
155
|
+
checkbox.onchange = updateBulkActions;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
document.getElementById('deleteSelectedBtn').onclick = () => deleteSelected(loadClientServers, null, window.loadClients); // Call deleteSelected from modals.js
|
|
159
|
+
document.getElementById('selectAllServersBtn').onclick = selectAllServers;
|
|
160
|
+
document.getElementById('deselectAllServersBtn').onclick = deselectAllServers;
|
|
161
|
+
|
|
162
|
+
const serverList = document.getElementById('serverList');
|
|
163
|
+
serverList.addEventListener('click', (e) => {
|
|
164
|
+
if (e.target.classList.contains('delete-server-btn')) {
|
|
165
|
+
deleteServerFromClientView(e.target.dataset.serverName);
|
|
166
|
+
} else if (e.target.classList.contains('edit-server-btn')) {
|
|
167
|
+
editServerFromClientView(e.target.dataset.serverName);
|
|
168
|
+
} else if (e.target.classList.contains('copy-server-btn')) {
|
|
169
|
+
copyServerFromClientView(e.target.dataset.serverName);
|
|
170
|
+
} else if (e.target.classList.contains('copy-to-clipboard-btn')) {
|
|
171
|
+
copyToClipboardFromClientView(e.target.dataset.serverName, e);
|
|
172
|
+
} else if (e.target.classList.contains('export-server-btn')) {
|
|
173
|
+
exportServerFromClientView(e.target.dataset.serverName);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Wrapper functions to pass callbacks
|
|
179
|
+
export const editServerFromClientView = async (name) => {
|
|
180
|
+
try {
|
|
181
|
+
const config = await getClientConfigApi(currentClient);
|
|
182
|
+
const serverConfig = config.servers[name];
|
|
183
|
+
showServerModal(name, serverConfig, loadClientServers, null, currentClient, loadClientsCallback);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
alert('Failed to load server config for editing: ' + error.message);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const copyServerFromClientView = (serverName) => {
|
|
190
|
+
copyServer(serverName, window.loadClients);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const copyToClipboardFromClientView = (serverName, event) => {
|
|
194
|
+
copyToClipboard(serverName, event);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const exportServerFromClientView = (serverName) => {
|
|
198
|
+
exportServer(serverName, currentClient);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export const deleteServerFromClientView = (name) => {
|
|
202
|
+
deleteServer(name, loadClientServers, null, loadClientsCallback, currentClient);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export const removeFromAllFromClientView = (serverName) => {
|
|
206
|
+
removeFromAll(serverName, window.loadClients, loadClientServers, window.loadClients);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export const renameServerFromClientView = (serverName) => {
|
|
210
|
+
showRenameServerModal(serverName, loadClientsCallback, currentClient);
|
|
211
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { getClientConfigApi, copyServerApi } from './api.js';
|
|
2
|
+
import { showServerModal, editServer, deleteServer, copyToClipboard, exportServer } from './modals.js';
|
|
3
|
+
|
|
4
|
+
let clients = []; // This will be passed from main.js
|
|
5
|
+
let draggedServer = null;
|
|
6
|
+
let draggedFromClient = null;
|
|
7
|
+
let loadClientsCallback = null; // Callback to main.js to reload all clients
|
|
8
|
+
|
|
9
|
+
export function initKanbanView(allClients, loadClientsFn) {
|
|
10
|
+
clients = allClients;
|
|
11
|
+
loadClientsCallback = loadClientsFn;
|
|
12
|
+
attachKanbanViewEventListeners();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Simple hash function to generate a consistent color from a string
|
|
16
|
+
function getServerColor(serverName) {
|
|
17
|
+
let hash = 0;
|
|
18
|
+
for (let i = 0; i < serverName.length; i++) {
|
|
19
|
+
hash = serverName.charCodeAt(i) + ((hash << 5) - hash);
|
|
20
|
+
}
|
|
21
|
+
const hue = hash % 360;
|
|
22
|
+
return `hsl(${hue}, 70%, 85%)`; // Light, saturated color
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function renderKanbanBoard() {
|
|
26
|
+
const board = document.getElementById('kanbanBoard');
|
|
27
|
+
board.innerHTML = '';
|
|
28
|
+
|
|
29
|
+
const kanbanSort = document.getElementById('kanbanSort');
|
|
30
|
+
let sortBy = localStorage.getItem('kanbanSortBy') || 'name-asc';
|
|
31
|
+
if (kanbanSort) {
|
|
32
|
+
kanbanSort.value = sortBy;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sortedClients = [...clients].sort((a, b) => {
|
|
36
|
+
if (sortBy === 'name-asc') {
|
|
37
|
+
return a.name.localeCompare(b.name);
|
|
38
|
+
} else if (sortBy === 'name-desc') {
|
|
39
|
+
return b.name.localeCompare(a.name);
|
|
40
|
+
} else if (sortBy === 'servers-asc') {
|
|
41
|
+
return (a.serverCount || 0) - (b.serverCount || 0);
|
|
42
|
+
} else if (sortBy === 'servers-desc') {
|
|
43
|
+
return (b.serverCount || 0) - (a.serverCount || 0);
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
for (const client of sortedClients) {
|
|
49
|
+
const column = document.createElement('div');
|
|
50
|
+
column.className = 'kanban-column';
|
|
51
|
+
column.dataset.client = client.id;
|
|
52
|
+
|
|
53
|
+
const header = document.createElement('div');
|
|
54
|
+
header.className = 'kanban-header';
|
|
55
|
+
header.innerHTML = `
|
|
56
|
+
<h3>${client.name}</h3>
|
|
57
|
+
<div class="kanban-config-path">${client.configPath}</div>
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const serversContainer = document.createElement('div');
|
|
61
|
+
serversContainer.className = 'kanban-servers';
|
|
62
|
+
serversContainer.dataset.client = client.id;
|
|
63
|
+
|
|
64
|
+
// Load servers for this client
|
|
65
|
+
try {
|
|
66
|
+
const config = await getClientConfigApi(client.id);
|
|
67
|
+
|
|
68
|
+
for (const [name, server] of Object.entries(config.servers || {})) {
|
|
69
|
+
const card = createKanbanCard(client.id, name, server);
|
|
70
|
+
serversContainer.appendChild(card);
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error(`Failed to load servers for ${client.id}:`, error);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Add drop zone events
|
|
77
|
+
serversContainer.ondragover = (e) => {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
serversContainer.classList.add('drag-over');
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
serversContainer.ondragleave = () => {
|
|
83
|
+
serversContainer.classList.remove('drag-over');
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
serversContainer.ondrop = async (e) => {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
serversContainer.classList.remove('drag-over');
|
|
89
|
+
|
|
90
|
+
if (draggedServer && draggedFromClient && draggedFromClient !== client.id) {
|
|
91
|
+
await handleDrop(client.id);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const addBtn = document.createElement('button');
|
|
96
|
+
addBtn.className = 'btn btn-primary kanban-add-btn';
|
|
97
|
+
addBtn.textContent = 'Add Server';
|
|
98
|
+
addBtn.addEventListener('click', () => {
|
|
99
|
+
// Pass client.id as the clientId parameter to showServerModal
|
|
100
|
+
showServerModal(null, null, () => window.loadClients(), renderKanbanBoard, client.id, loadClientsCallback); // Pass client.id and renderKanbanBoard as callback
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
column.appendChild(header);
|
|
104
|
+
column.appendChild(serversContainer);
|
|
105
|
+
column.appendChild(addBtn);
|
|
106
|
+
board.appendChild(column);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (kanbanSort) {
|
|
110
|
+
kanbanSort.onchange = () => {
|
|
111
|
+
localStorage.setItem('kanbanSortBy', kanbanSort.value);
|
|
112
|
+
renderKanbanBoard();
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createKanbanCard(clientId, serverName, server) {
|
|
118
|
+
const card = document.createElement('div');
|
|
119
|
+
card.className = 'kanban-card';
|
|
120
|
+
card.draggable = true;
|
|
121
|
+
card.dataset.server = serverName;
|
|
122
|
+
card.dataset.client = clientId;
|
|
123
|
+
card.style.backgroundColor = getServerColor(serverName);
|
|
124
|
+
|
|
125
|
+
let details = '';
|
|
126
|
+
if (server.command) details += `cmd: ${server.command}\n`;
|
|
127
|
+
if (server.args) details += `args: ${server.args.join(' ')}\n`;
|
|
128
|
+
if (server.env) details += `env: ${Object.keys(server.env).length} var(s)`;
|
|
129
|
+
|
|
130
|
+
card.innerHTML = `
|
|
131
|
+
<div class="kanban-card-header">
|
|
132
|
+
<span class="kanban-card-title">${serverName}</span>
|
|
133
|
+
<div class="kanban-card-actions">
|
|
134
|
+
<button class="icon-btn edit-server-kanban-btn" title="Edit" data-client-id="${clientId}" data-server-name="${serverName}">✏️</button>
|
|
135
|
+
<button class="icon-btn copy-to-clipboard-kanban-btn" title="Copy to clipboard" data-client-id="${clientId}" data-server-name="${serverName}">📋</button>
|
|
136
|
+
<button class="icon-btn export-server-kanban-btn" title="Export" data-client-id="${clientId}" data-server-name="${serverName}">💾</button>
|
|
137
|
+
<button class="icon-btn delete delete-server-kanban-btn" title="Delete" data-client-id="${clientId}" data-server-name="${serverName}">🗑️</button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="kanban-card-details">${details}</div>
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
card.ondragstart = (e) => {
|
|
144
|
+
// Don't start drag if clicking on buttons
|
|
145
|
+
if (e.target.tagName === 'BUTTON') {
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
draggedServer = serverName;
|
|
150
|
+
draggedFromClient = clientId;
|
|
151
|
+
card.classList.add('dragging');
|
|
152
|
+
e.dataTransfer.effectAllowed = 'copy';
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
card.ondragend = () => {
|
|
156
|
+
card.classList.remove('dragging');
|
|
157
|
+
draggedServer = null;
|
|
158
|
+
draggedFromClient = null;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return card;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function attachKanbanViewEventListeners() {
|
|
165
|
+
const kanbanBoard = document.getElementById('kanbanBoard');
|
|
166
|
+
kanbanBoard.addEventListener('click', (e) => {
|
|
167
|
+
if (e.target.classList.contains('edit-server-kanban-btn')) {
|
|
168
|
+
editServerKanban(e.target.dataset.clientId, e.target.dataset.serverName, e);
|
|
169
|
+
} else if (e.target.classList.contains('rename-server-kanban-btn')) {
|
|
170
|
+
renameServerKanban(e.target.dataset.clientId, e.target.dataset.serverName, e);
|
|
171
|
+
} else if (e.target.classList.contains('copy-to-clipboard-kanban-btn')) {
|
|
172
|
+
copyToClipboardKanban(e.target.dataset.clientId, e.target.dataset.serverName, e);
|
|
173
|
+
} else if (e.target.classList.contains('export-server-kanban-btn')) {
|
|
174
|
+
exportServerKanban(e.target.dataset.clientId, e.target.dataset.serverName, e);
|
|
175
|
+
} else if (e.target.classList.contains('delete-server-kanban-btn')) {
|
|
176
|
+
deleteServerKanban(e.target.dataset.clientId, e.target.dataset.serverName, e);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const handleDrop = async (targetClient) => {
|
|
182
|
+
if (!draggedServer || !draggedFromClient) return;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const response = await copyServerApi(draggedFromClient, draggedServer, targetClient, draggedServer);
|
|
186
|
+
|
|
187
|
+
if (response.success) {
|
|
188
|
+
await renderKanbanBoard();
|
|
189
|
+
} else {
|
|
190
|
+
throw new Error('Failed to copy server');
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
alert('Failed to copy server: ' + error.message);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const editServerKanban = (clientId, serverName, event) => {
|
|
198
|
+
event.stopPropagation();
|
|
199
|
+
editServer(serverName, renderKanbanBoard, clientId, loadClientsCallback);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export const deleteServerKanban = (clientId, serverName, event) => {
|
|
203
|
+
event.stopPropagation();
|
|
204
|
+
if (!confirm(`Are you sure you want to delete the server "${serverName}" from ${clients.find(c => c.id === clientId).name}?`)) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
deleteServer(serverName, () => window.loadClients(), null, window.loadClients, clientId); // Pass clientId
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const exportServerKanban = (clientId, serverName, event) => {
|
|
212
|
+
event.stopPropagation();
|
|
213
|
+
exportServer(serverName, clientId);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export const copyToClipboardKanban = (clientId, serverName, event) => {
|
|
217
|
+
event.stopPropagation();
|
|
218
|
+
copyToClipboard(serverName, event, null, clientId);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export const renameServerKanban = (clientId, serverName, event) => {
|
|
222
|
+
event.stopPropagation();
|
|
223
|
+
showRenameServerModal(serverName, () => window.loadClients(), clientId); // Pass clientId
|
|
224
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { detectClientsApi, listClientsApi } from './api.js';
|
|
2
|
+
import { initClientView, renderClientList, selectClient, loadClientServers } from './clientView.js';
|
|
3
|
+
import { initKanbanView, renderKanbanBoard } from './kanbanView.js';
|
|
4
|
+
import { initServerView, renderAllServers } from './serverView.js';
|
|
5
|
+
import { showServerModal, showImportModal, exportConfig, initModals, showRemoteServerModal } from './modals.js';
|
|
6
|
+
|
|
7
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
8
|
+
let currentClient = null;
|
|
9
|
+
let clients = [];
|
|
10
|
+
let currentView = 'list';
|
|
11
|
+
|
|
12
|
+
const listViewBtn = document.getElementById('listViewBtn');
|
|
13
|
+
const kanbanViewBtn = document.getElementById('kanbanViewBtn');
|
|
14
|
+
const serverViewBtn = document.getElementById('serverViewBtn');
|
|
15
|
+
|
|
16
|
+
const listViewContainer = document.getElementById('listViewContainer');
|
|
17
|
+
const kanbanViewContainer = document.getElementById('kanbanViewContainer');
|
|
18
|
+
const serverViewContainer = document.getElementById('serverViewContainer');
|
|
19
|
+
|
|
20
|
+
async function refreshClients() {
|
|
21
|
+
try {
|
|
22
|
+
await detectClientsApi();
|
|
23
|
+
await loadClients();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Failed to refresh clients:', error);
|
|
26
|
+
alert('Failed to refresh clients. Make sure the server is running.');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function loadClients() {
|
|
31
|
+
try {
|
|
32
|
+
clients = await listClientsApi();
|
|
33
|
+
|
|
34
|
+
// Initialize modules with current client data
|
|
35
|
+
initClientView(clients, currentClient, loadClients);
|
|
36
|
+
initKanbanView(clients, loadClients);
|
|
37
|
+
initServerView(clients, loadClients);
|
|
38
|
+
initModals(clients, currentClient, window.loadClients);
|
|
39
|
+
|
|
40
|
+
renderClientList();
|
|
41
|
+
renderKanbanBoard();
|
|
42
|
+
renderAllServers();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Failed to load clients:', error);
|
|
45
|
+
alert('Failed to load clients. Make sure the server is running.');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
window.loadClients = loadClients; // Make loadClients globally accessible
|
|
49
|
+
window.setCurrentClient = (clientId) => { currentClient = clientId; }; // Make setCurrentClient globally accessible
|
|
50
|
+
|
|
51
|
+
// View switching
|
|
52
|
+
function switchView(view) {
|
|
53
|
+
currentView = view;
|
|
54
|
+
|
|
55
|
+
listViewBtn.classList.toggle('active', view === 'list');
|
|
56
|
+
kanbanViewBtn.classList.toggle('active', view === 'kanban');
|
|
57
|
+
serverViewBtn.classList.toggle('active', view === 'server');
|
|
58
|
+
|
|
59
|
+
listViewContainer.style.display = view === 'list' ? 'block' : 'none';
|
|
60
|
+
kanbanViewContainer.style.display = view === 'kanban' ? 'block' : 'none';
|
|
61
|
+
serverViewContainer.style.display = view === 'server' ? 'block' : 'none';
|
|
62
|
+
|
|
63
|
+
if (view === 'kanban') {
|
|
64
|
+
renderKanbanBoard();
|
|
65
|
+
} else if (view === 'server') {
|
|
66
|
+
renderAllServers();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Global Event Listeners
|
|
71
|
+
listViewBtn.addEventListener('click', () => switchView('list'));
|
|
72
|
+
kanbanViewBtn.addEventListener('click', () => switchView('kanban'));
|
|
73
|
+
serverViewBtn.addEventListener('click', () => switchView('server'));
|
|
74
|
+
|
|
75
|
+
document.getElementById('refreshBtn').addEventListener('click', refreshClients);
|
|
76
|
+
document.getElementById('addServerBtn').addEventListener('click', () => {
|
|
77
|
+
// For global add server button, we need to prompt user to select a client first
|
|
78
|
+
if (!currentClient) {
|
|
79
|
+
alert('Please select a client from the sidebar first, then use the Add Server button in the client view.');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
showServerModal(null, null, loadClientServers, renderKanbanBoard, currentClient, window.loadClients);
|
|
83
|
+
});
|
|
84
|
+
document.getElementById('addRemoteServerBtn').addEventListener('click', () => {
|
|
85
|
+
// For global add remote server button, we need to prompt user to select a client first
|
|
86
|
+
if (!currentClient) {
|
|
87
|
+
alert('Please select a client from the sidebar first, then use the Add Remote MCP button in the client view.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
showRemoteServerModal(loadClientServers, renderKanbanBoard, currentClient, window.loadClients);
|
|
91
|
+
});
|
|
92
|
+
document.getElementById('exportConfigBtn').addEventListener('click', exportConfig);
|
|
93
|
+
document.getElementById('importConfigBtn').addEventListener('click', () => showImportModal(loadClientServers, renderKanbanBoard, window.loadClients));
|
|
94
|
+
|
|
95
|
+
// Close modals when clicking outside
|
|
96
|
+
document.querySelectorAll('.modal').forEach(modal => {
|
|
97
|
+
modal.addEventListener('click', (e) => {
|
|
98
|
+
if (e.target === modal) {
|
|
99
|
+
modal.style.display = 'none';
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
document.getElementById('cancelModal').addEventListener('click', () => {
|
|
105
|
+
document.getElementById('serverModal').style.display = 'none';
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
document.getElementById('cancelCopy').addEventListener('click', () => {
|
|
109
|
+
document.getElementById('copyModal').style.display = 'none';
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
document.getElementById('cancelImport').addEventListener('click', () => {
|
|
113
|
+
document.getElementById('importModal').style.display = 'none';
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
document.getElementById('cancelEditServerEnv').addEventListener('click', () => {
|
|
117
|
+
document.getElementById('editServerEnvModal').style.display = 'none';
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
document.getElementById('cancelAddServerToClients').addEventListener('click', () => {
|
|
121
|
+
document.getElementById('addServerToClientsModal').style.display = 'none';
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Load clients on page load
|
|
125
|
+
loadClients();
|
|
126
|
+
});
|