mcp-config-manager 1.0.13 → 2.1.1

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,304 @@
1
+ import { addServerToMultipleClientsApi } from './api.js';
2
+ import { loadStaticLogos, getServerLogo, getInitials } from './logoService.js';
3
+
4
+ let cachedData = null;
5
+ let clients = [];
6
+ let loadClientsCallback = null;
7
+
8
+ export function initPopularMcps(clientData, loadClientsFn) {
9
+ clients = clientData;
10
+ loadClientsCallback = loadClientsFn;
11
+ }
12
+
13
+ export async function loadPopularMcps() {
14
+ if (cachedData) return cachedData;
15
+ try {
16
+ const response = await fetch('/popular-mcps.json');
17
+ cachedData = await response.json();
18
+ return cachedData;
19
+ } catch (error) {
20
+ console.error('Failed to load popular MCPs:', error);
21
+ return null;
22
+ }
23
+ }
24
+
25
+
26
+ export async function showPopularMcpsModal(clientData, loadClientsFn) {
27
+ clients = clientData;
28
+ loadClientsCallback = loadClientsFn;
29
+
30
+ const modal = document.getElementById('popularMcpsModal');
31
+ const listContainer = document.getElementById('popularMcpsList');
32
+ const searchInput = document.getElementById('popularMcpsSearch');
33
+ const categorySelect = document.getElementById('popularMcpsCategory');
34
+ const authSelect = document.getElementById('popularMcpsAuth');
35
+
36
+ // Show loading state
37
+ listContainer.innerHTML = '<p class="loading-state">Loading popular MCPs...</p>';
38
+ modal.style.display = 'flex';
39
+
40
+ // Load data and logos in parallel
41
+ const [data] = await Promise.all([loadPopularMcps(), loadStaticLogos()]);
42
+ if (!data) {
43
+ listContainer.innerHTML = '<p class="error-state">Failed to load popular MCPs. Please try again.</p>';
44
+ return;
45
+ }
46
+
47
+ // Populate category dropdown
48
+ const categories = [...new Set(data.servers.map(s => s.category))].sort();
49
+ categorySelect.innerHTML = '<option value="">All Categories</option>' +
50
+ categories.map(c => `<option value="${c}">${formatCategory(c)}</option>`).join('');
51
+
52
+ // Initial render
53
+ renderServerList(data.servers);
54
+
55
+ // Set up event listeners (remove old ones first)
56
+ const newSearchInput = searchInput.cloneNode(true);
57
+ const newCategorySelect = categorySelect.cloneNode(true);
58
+ const newAuthSelect = authSelect.cloneNode(true);
59
+
60
+ searchInput.parentNode.replaceChild(newSearchInput, searchInput);
61
+ categorySelect.parentNode.replaceChild(newCategorySelect, categorySelect);
62
+ authSelect.parentNode.replaceChild(newAuthSelect, authSelect);
63
+
64
+ // Restore category options
65
+ newCategorySelect.innerHTML = '<option value="">All Categories</option>' +
66
+ categories.map(c => `<option value="${c}">${formatCategory(c)}</option>`).join('');
67
+
68
+ const filterAndRender = () => {
69
+ const search = newSearchInput.value.toLowerCase();
70
+ const category = newCategorySelect.value;
71
+ const auth = newAuthSelect.value;
72
+
73
+ const filtered = data.servers.filter(server => {
74
+ const matchesSearch = !search ||
75
+ server.name.toLowerCase().includes(search) ||
76
+ server.description.toLowerCase().includes(search);
77
+ const matchesCategory = !category || server.category === category;
78
+ const matchesAuth = !auth || server.authType === auth;
79
+ return matchesSearch && matchesCategory && matchesAuth;
80
+ });
81
+
82
+ renderServerList(filtered);
83
+ };
84
+
85
+ newSearchInput.addEventListener('input', filterAndRender);
86
+ newCategorySelect.addEventListener('change', filterAndRender);
87
+ newAuthSelect.addEventListener('change', filterAndRender);
88
+ }
89
+
90
+ function renderServerList(servers) {
91
+ const listContainer = document.getElementById('popularMcpsList');
92
+
93
+ if (servers.length === 0) {
94
+ listContainer.innerHTML = '<p class="empty-state">No servers match your filters.</p>';
95
+ return;
96
+ }
97
+
98
+ const initials = {};
99
+ servers.forEach(s => initials[s.id] = getInitials(s.name));
100
+
101
+ listContainer.innerHTML = servers.map(server => `
102
+ <div class="popular-mcp-card" data-server-id="${server.id}">
103
+ <div class="popular-mcp-logo-container">
104
+ <img src="" alt="${server.name}" class="popular-mcp-logo" style="display:none" data-server-id="${server.id}" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex'">
105
+ <div class="popular-mcp-logo-fallback">${initials[server.id]}</div>
106
+ </div>
107
+ <div class="popular-mcp-info">
108
+ <div class="popular-mcp-name">${server.name}</div>
109
+ <div class="popular-mcp-description">${server.description}</div>
110
+ <div class="popular-mcp-meta">
111
+ <span class="category-badge">${formatCategory(server.category)}</span>
112
+ <span class="auth-badge auth-badge-${server.authType}">${formatAuthType(server.authType)}</span>
113
+ <span class="transport-badge transport-${server.type}">${server.type.toUpperCase()}</span>
114
+ </div>
115
+ </div>
116
+ <div class="popular-mcp-actions">
117
+ <button class="btn btn-small btn-primary add-popular-mcp-btn"
118
+ data-server='${JSON.stringify(server).replace(/'/g, "&#39;")}'>
119
+ Add
120
+ </button>
121
+ </div>
122
+ </div>`).join('');
123
+
124
+ // Load logos asynchronously
125
+ servers.forEach(server => {
126
+ const imgElement = listContainer.querySelector(`img[data-server-id="${server.id}"]`);
127
+ if (!imgElement) return;
128
+
129
+ // Use server.logo first if available, otherwise use the shared service
130
+ if (server.logo) {
131
+ imgElement.src = server.logo;
132
+ imgElement.style.display = 'block';
133
+ const fallback = imgElement.nextElementSibling;
134
+ if (fallback) fallback.style.display = 'none';
135
+ } else {
136
+ // Use shared logo service
137
+ getServerLogo(server.id, { type: server.type, url: server.url }).then(logoUrl => {
138
+ if (logoUrl && imgElement && imgElement.parentNode) {
139
+ imgElement.src = logoUrl;
140
+ imgElement.style.display = 'block';
141
+ const fallback = imgElement.nextElementSibling;
142
+ if (fallback) fallback.style.display = 'none';
143
+ }
144
+ });
145
+ }
146
+ });
147
+
148
+ // Attach event listeners to add buttons
149
+ listContainer.querySelectorAll('.add-popular-mcp-btn').forEach(btn => {
150
+ btn.addEventListener('click', (e) => {
151
+ const server = JSON.parse(e.target.dataset.server.replace(/&#39;/g, "'"));
152
+ showAddToClientsDropdown(e.target, server);
153
+ });
154
+ });
155
+ }
156
+
157
+ function showAddToClientsDropdown(button, server) {
158
+ // Remove any existing dropdown
159
+ const existingDropdown = document.querySelector('.add-clients-dropdown');
160
+ if (existingDropdown) {
161
+ existingDropdown.remove();
162
+ }
163
+
164
+ // Create dropdown
165
+ const dropdown = document.createElement('div');
166
+ dropdown.className = 'add-clients-dropdown';
167
+ dropdown.innerHTML = `
168
+ <div class="add-clients-dropdown-header">
169
+ <span>Add to clients:</span>
170
+ <button class="add-clients-select-all">All</button>
171
+ <button class="add-clients-select-none">None</button>
172
+ </div>
173
+ <div class="add-clients-list">
174
+ ${clients.map(client => `
175
+ <label class="add-clients-item">
176
+ <input type="checkbox" value="${client.id}" checked>
177
+ <span>${client.name}</span>
178
+ </label>
179
+ `).join('')}
180
+ </div>
181
+ <div class="add-clients-dropdown-actions">
182
+ <button class="btn btn-small btn-secondary cancel-add-btn">Cancel</button>
183
+ <button class="btn btn-small btn-primary confirm-add-btn">Add to Selected</button>
184
+ </div>
185
+ `;
186
+
187
+ // Position dropdown
188
+ const buttonRect = button.getBoundingClientRect();
189
+ dropdown.style.position = 'fixed';
190
+ dropdown.style.top = `${buttonRect.bottom + 4}px`;
191
+ dropdown.style.right = `${window.innerWidth - buttonRect.right}px`;
192
+
193
+ document.body.appendChild(dropdown);
194
+
195
+ // Event handlers
196
+ dropdown.querySelector('.add-clients-select-all').addEventListener('click', () => {
197
+ dropdown.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = true);
198
+ });
199
+
200
+ dropdown.querySelector('.add-clients-select-none').addEventListener('click', () => {
201
+ dropdown.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
202
+ });
203
+
204
+ dropdown.querySelector('.cancel-add-btn').addEventListener('click', () => {
205
+ dropdown.remove();
206
+ });
207
+
208
+ dropdown.querySelector('.confirm-add-btn').addEventListener('click', async () => {
209
+ const selectedClientIds = Array.from(dropdown.querySelectorAll('input[type="checkbox"]:checked'))
210
+ .map(cb => cb.value);
211
+
212
+ if (selectedClientIds.length === 0) {
213
+ alert('Please select at least one client.');
214
+ return;
215
+ }
216
+
217
+ await addServerToClients(server, selectedClientIds, button);
218
+ dropdown.remove();
219
+ });
220
+
221
+ // Close dropdown when clicking outside
222
+ const closeOnClickOutside = (e) => {
223
+ if (!dropdown.contains(e.target) && e.target !== button) {
224
+ dropdown.remove();
225
+ document.removeEventListener('click', closeOnClickOutside);
226
+ }
227
+ };
228
+ setTimeout(() => document.addEventListener('click', closeOnClickOutside), 0);
229
+ }
230
+
231
+ async function addServerToClients(server, clientIds, button) {
232
+ const originalText = button.textContent;
233
+ button.textContent = 'Adding...';
234
+ button.disabled = true;
235
+
236
+ try {
237
+ const config = {
238
+ type: server.type,
239
+ url: server.url
240
+ };
241
+
242
+ const response = await addServerToMultipleClientsApi(server.id, config, clientIds);
243
+
244
+ if (response.success) {
245
+ button.textContent = 'Added!';
246
+ button.classList.add('btn-success');
247
+
248
+ // Show auth note if needed
249
+ if (server.authType !== 'none') {
250
+ const authNote = server.authType === 'oauth'
251
+ ? 'OAuth authentication will be required when you first use this server.'
252
+ : 'You\'ll need to configure your API key in the server\'s environment variables.';
253
+ alert(`${server.name} added successfully!\n\nNote: ${authNote}`);
254
+ }
255
+
256
+ // Refresh clients
257
+ if (loadClientsCallback) {
258
+ loadClientsCallback();
259
+ }
260
+
261
+ // Reset button after delay
262
+ setTimeout(() => {
263
+ button.textContent = originalText;
264
+ button.classList.remove('btn-success');
265
+ button.disabled = false;
266
+ }, 2000);
267
+ } else {
268
+ throw new Error('Failed to add server');
269
+ }
270
+ } catch (error) {
271
+ console.error('Failed to add server:', error);
272
+ button.textContent = 'Failed';
273
+ button.classList.add('btn-danger');
274
+ alert(`Failed to add ${server.name}: ${error.message}`);
275
+
276
+ setTimeout(() => {
277
+ button.textContent = originalText;
278
+ button.classList.remove('btn-danger');
279
+ button.disabled = false;
280
+ }, 2000);
281
+ }
282
+ }
283
+
284
+ function formatCategory(category) {
285
+ return category
286
+ .split('-')
287
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
288
+ .join(' ');
289
+ }
290
+
291
+ function formatAuthType(authType) {
292
+ switch (authType) {
293
+ case 'none': return 'No Auth';
294
+ case 'api-key': return 'API Key';
295
+ case 'oauth': return 'OAuth';
296
+ case 'bearer': return 'Bearer';
297
+ default: return authType;
298
+ }
299
+ }
300
+
301
+ export function closePopularMcpsModal() {
302
+ const modal = document.getElementById('popularMcpsModal');
303
+ modal.style.display = 'none';
304
+ }
@@ -1,6 +1,7 @@
1
1
  import { getAllServersApi, updateServerEnvApi, addServerToMultipleClientsApi } from './api.js';
2
2
  import { addEnvVarRow } from './utils.js';
3
- import { showServerModal, deleteServer, copyToClipboard, exportServer, removeFromAll } from './modals.js'; // Import from modals.js
3
+ import { showServerModal, deleteServer, copyToClipboard, exportServer, removeFromAll } from './modals.js';
4
+ import { getServerLogo, getInitials } from './logoService.js';
4
5
 
5
6
  let clients = []; // This will be passed from main.js
6
7
  let loadClientsCallback = null; // Callback to main.js to reload all clients
@@ -55,9 +56,14 @@ export async function renderAllServers() {
55
56
  }
56
57
  }
57
58
 
59
+ const initials = getInitials(serverName);
58
60
  card.innerHTML = `
59
61
  <div class="server-header-all">
60
62
  <div class="server-name-row">
63
+ <div class="server-logo-container">
64
+ <img src="" class="server-logo" alt="" style="display:none;width:32px;height:32px" data-server="${serverName}" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex'">
65
+ <div class="server-logo-fallback" style="width:32px;height:32px;font-size:12px">${initials}</div>
66
+ </div>
61
67
  <span class="server-name-all">${serverName}</span>
62
68
  ${serverData.global ? '<span class="global-tag">Global</span>' : ''}
63
69
  </div>
@@ -78,6 +84,17 @@ export async function renderAllServers() {
78
84
  </div>
79
85
  `;
80
86
  allServersList.appendChild(card);
87
+
88
+ // Load logo asynchronously
89
+ const imgElement = card.querySelector('.server-logo');
90
+ getServerLogo(serverName, serverData.config).then(logoUrl => {
91
+ if (logoUrl && imgElement) {
92
+ imgElement.src = logoUrl;
93
+ imgElement.style.display = 'block';
94
+ const fallback = imgElement.nextElementSibling;
95
+ if (fallback) fallback.style.display = 'none';
96
+ }
97
+ });
81
98
  }
82
99
  attachServerViewEventListeners();
83
100
  } catch (error) {
@@ -99,30 +116,30 @@ function attachServerViewEventListeners() {
99
116
 
100
117
  document.querySelectorAll('.add-to-clients-btn').forEach(button => {
101
118
  button.addEventListener('click', (e) => {
102
- const serverName = e.target.dataset.serverName;
103
- const serverConfig = JSON.parse(e.target.dataset.serverConfig);
119
+ const serverName = e.currentTarget.dataset.serverName;
120
+ const serverConfig = JSON.parse(e.currentTarget.dataset.serverConfig);
104
121
  showAddServerToClientsModal(serverName, serverConfig);
105
122
  });
106
123
  });
107
124
 
108
125
  document.querySelectorAll('.copy-to-clipboard-server-view-btn').forEach(button => {
109
126
  button.addEventListener('click', (e) => {
110
- const serverName = e.target.dataset.serverName;
111
- const serverConfig = JSON.parse(e.target.dataset.serverConfig);
127
+ const serverName = e.currentTarget.dataset.serverName;
128
+ const serverConfig = JSON.parse(e.currentTarget.dataset.serverConfig);
112
129
  copyToClipboard(serverName, e, serverConfig); // Pass serverConfig
113
130
  });
114
131
  });
115
132
 
116
133
  document.querySelectorAll('.export-server-view-btn').forEach(button => {
117
134
  button.addEventListener('click', (e) => {
118
- const serverName = e.target.dataset.serverName;
135
+ const serverName = e.currentTarget.dataset.serverName;
119
136
  exportServer(serverName);
120
137
  });
121
138
  });
122
139
 
123
140
  document.querySelectorAll('.delete-server-view-btn').forEach(button => {
124
141
  button.addEventListener('click', (e) => {
125
- const serverName = e.target.dataset.serverName;
142
+ const serverName = e.currentTarget.dataset.serverName;
126
143
  if (!confirm(`Are you sure you want to delete the server "${serverName}" from ALL clients?`)) {
127
144
  return;
128
145
  }
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "filesystem": "https://raw.githubusercontent.com/modelcontextprotocol/servers/main/src/filesystem/icon.png",
3
3
  "github": "https://github.githubassets.com/favicons/favicon.svg",
4
+ "atlassian": "https://wac-cdn.atlassian.com/assets/img/favicons/atlassian/favicon.png",
5
+ "paypal": "https://www.paypal.com/favicon.ico",
6
+ "square": "https://squareup.com/favicon.ico",
7
+ "neon": "https://neon.tech/favicon.ico",
8
+ "prisma": "https://www.prisma.io/images/favicon-32x32.png",
9
+ "webflow": "https://webflow.com/favicon.ico",
10
+ "wix": "https://www.wix.com/favicon.ico",
11
+ "canva": "https://www.canva.com/favicon.ico",
12
+ "zapier": "https://zapier.com/favicon.ico",
13
+ "huggingface": "https://huggingface.co/favicon.ico",
14
+ "exa": "https://exa.ai/favicon.ico",
15
+ "semgrep": "https://semgrep.dev/favicon.ico",
16
+ "buildkite": "https://buildkite.com/favicon.ico",
17
+ "egnyte": "https://www.egnyte.com/favicon.ico",
18
+ "plaid": "https://plaid.com/favicon.ico",
19
+ "ramp": "https://ramp.com/favicon.ico",
20
+ "stackoverflow": "https://cdn.sstatic.net/Sites/stackoverflow/Img/favicon.ico",
21
+ "thoughtspot": "https://www.thoughtspot.com/favicon.ico",
22
+ "needle": "https://needle-ai.com/favicon.ico",
23
+ "dappier": "https://dappier.com/favicon.ico",
24
+ "cloudinary": "https://cloudinary.com/favicon.ico",
25
+ "instant": "https://instantdb.com/favicon.ico",
26
+ "grafbase": "https://grafbase.com/favicon.ico",
27
+ "telnyx": "https://telnyx.com/favicon.ico",
28
+ "apify": "https://apify.com/favicon.ico",
29
+ "monday": "https://monday.com/favicon.ico",
4
30
  "gitlab": "https://about.gitlab.com/images/press/press-kit-icon.svg",
5
31
  "slack": "https://a.slack-edge.com/80588/marketing/img/meta/favicon-32.png",
6
32
  "postgres": "https://www.postgresql.org/media/img/about/press/elephant.png",