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,392 @@
1
+ /**
2
+ * Logo Service - Automatic icon detection for MCP servers
3
+ * Tries multiple strategies to find an appropriate logo
4
+ */
5
+
6
+ import { getIconPreference } from './iconPreferences.js';
7
+
8
+ let staticLogos = null;
9
+ const logoCache = new Map();
10
+
11
+ // Logo.dev API configuration (free tier)
12
+ const LOGO_DEV_API_KEY = 'pk_Rd3I7mQnQ3a-3VJxZemUPA';
13
+ const LOGO_DEV_BASE_URL = 'https://img.logo.dev';
14
+
15
+ // Load static logos from mcp-logos.json
16
+ export async function loadStaticLogos() {
17
+ if (staticLogos) return staticLogos;
18
+ try {
19
+ const response = await fetch('/mcp-logos.json');
20
+ staticLogos = await response.json();
21
+ return staticLogos;
22
+ } catch (error) {
23
+ console.error('Failed to load static logos:', error);
24
+ staticLogos = {};
25
+ return staticLogos;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Get logo URL for a server
31
+ * @param {string} serverName - The server name/id
32
+ * @param {object} serverConfig - Optional server config with command/args
33
+ * @param {boolean} skipCustomPrefs - Skip custom preferences check (used internally)
34
+ * @returns {Promise<string|null>} Logo URL or null
35
+ */
36
+ export async function getServerLogo(serverName, serverConfig = null, skipCustomPrefs = false) {
37
+ // Check cache first
38
+ const cacheKey = serverName.toLowerCase();
39
+ if (logoCache.has(cacheKey)) {
40
+ return logoCache.get(cacheKey);
41
+ }
42
+
43
+ // Strategy 0: Check custom icon preferences first (unless skipped)
44
+ if (!skipCustomPrefs) {
45
+ const customPref = getIconPreference(serverName);
46
+ if (customPref) {
47
+ let logoUrl = null;
48
+ switch (customPref.type) {
49
+ case 'none':
50
+ // User wants no icon, return null
51
+ logoCache.set(cacheKey, null);
52
+ return null;
53
+ case 'url':
54
+ // User provided a custom URL
55
+ logoUrl = customPref.value;
56
+ logoCache.set(cacheKey, logoUrl);
57
+ return logoUrl;
58
+ case 'logodev':
59
+ // User selected a specific domain from logo.dev
60
+ logoUrl = getLogoDevUrl(customPref.value);
61
+ logoCache.set(cacheKey, logoUrl);
62
+ return logoUrl;
63
+ // 'auto' falls through to normal detection
64
+ }
65
+ }
66
+ }
67
+
68
+ // Ensure static logos are loaded
69
+ await loadStaticLogos();
70
+
71
+ // Try various strategies
72
+ let logoUrl = null;
73
+
74
+ // Strategy 1: Direct match in static logos
75
+ logoUrl = findInStaticLogos(serverName);
76
+ if (logoUrl) {
77
+ logoCache.set(cacheKey, logoUrl);
78
+ return logoUrl;
79
+ }
80
+
81
+ // Strategy 2: Extract service name from server config
82
+ const serviceName = extractServiceName(serverName, serverConfig);
83
+ if (serviceName && serviceName !== serverName.toLowerCase()) {
84
+ logoUrl = findInStaticLogos(serviceName);
85
+ if (logoUrl) {
86
+ logoCache.set(cacheKey, logoUrl);
87
+ return logoUrl;
88
+ }
89
+ }
90
+
91
+ // Strategy 3: Try logo.dev API for known domains (higher quality logos)
92
+ const domain = guessDomain(serviceName || serverName);
93
+ if (domain) {
94
+ logoUrl = getLogoDevUrl(domain);
95
+ logoCache.set(cacheKey, logoUrl);
96
+ return logoUrl;
97
+ }
98
+
99
+ // No logo found
100
+ logoCache.set(cacheKey, null);
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Find logo in static logos with fuzzy matching
106
+ */
107
+ function findInStaticLogos(name) {
108
+ if (!staticLogos) return null;
109
+
110
+ const normalized = name.toLowerCase().replace(/[^a-z0-9]/g, '');
111
+
112
+ // Exact match
113
+ if (staticLogos[normalized]) return staticLogos[normalized];
114
+ if (staticLogos[name.toLowerCase()]) return staticLogos[name.toLowerCase()];
115
+
116
+ // Partial match - check if any key is contained in name or vice versa
117
+ for (const [key, url] of Object.entries(staticLogos)) {
118
+ if (normalized.includes(key) || key.includes(normalized)) {
119
+ return url;
120
+ }
121
+ }
122
+
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * Extract service name from server name or config
128
+ */
129
+ function extractServiceName(serverName, serverConfig) {
130
+ const name = serverName.toLowerCase();
131
+
132
+ // Remove common suffixes/prefixes
133
+ let cleaned = name
134
+ .replace(/[-_]?(mcp|server|client|local|remote|dev|prod)[-_]?/g, '')
135
+ .replace(/^@[^/]+\//, '') // Remove npm scope
136
+ .replace(/[-_]+/g, '-')
137
+ .replace(/^-|-$/g, '')
138
+ .trim();
139
+
140
+ if (cleaned) return cleaned;
141
+
142
+ // Try to extract from npm package in config
143
+ if (serverConfig) {
144
+ const args = serverConfig.args || [];
145
+ const command = serverConfig.command || '';
146
+
147
+ // Check for npx package names
148
+ for (const arg of args) {
149
+ // Match patterns like @modelcontextprotocol/server-github
150
+ const match = arg.match(/@[^/]+\/(?:server-|mcp-)?([a-z0-9-]+)/i) ||
151
+ arg.match(/(?:server-|mcp-)([a-z0-9-]+)/i);
152
+ if (match) {
153
+ return match[1].toLowerCase();
154
+ }
155
+ }
156
+
157
+ // Check command itself
158
+ const cmdMatch = command.match(/(?:server-|mcp-)([a-z0-9-]+)/i);
159
+ if (cmdMatch) {
160
+ return cmdMatch[1].toLowerCase();
161
+ }
162
+ }
163
+
164
+ return cleaned || name;
165
+ }
166
+
167
+ /**
168
+ * Build logo.dev URL for a domain
169
+ * @param {string} domain - The domain to get logo for
170
+ * @param {object} options - Optional parameters (size, format, grayscale)
171
+ * @returns {string} Logo.dev URL
172
+ */
173
+ function getLogoDevUrl(domain, options = {}) {
174
+ const params = new URLSearchParams({
175
+ token: LOGO_DEV_API_KEY,
176
+ size: options.size || 64,
177
+ format: options.format || 'png'
178
+ });
179
+
180
+ if (options.grayscale) {
181
+ params.append('greyscale', 'true');
182
+ }
183
+
184
+ return `${LOGO_DEV_BASE_URL}/${domain}?${params.toString()}`;
185
+ }
186
+
187
+ /**
188
+ * Guess domain from service name
189
+ */
190
+ function guessDomain(serviceName) {
191
+ const name = serviceName.toLowerCase().replace(/[^a-z0-9]/g, '');
192
+
193
+ // Names that should NOT auto-detect (too ambiguous or wrong matches)
194
+ const skipAutoDetect = new Set([
195
+ 'chrome', 'chromedevtools', 'devtools', // Would incorrectly show Google
196
+ 'server', 'mcp', 'local', 'test', 'dev', 'prod', // Too generic
197
+ ]);
198
+
199
+ if (skipAutoDetect.has(name)) {
200
+ return null;
201
+ }
202
+
203
+ // Known mappings
204
+ const domainMap = {
205
+ 'github': 'github.com',
206
+ 'gitlab': 'gitlab.com',
207
+ 'bitbucket': 'bitbucket.org',
208
+ 'notion': 'notion.so',
209
+ 'slack': 'slack.com',
210
+ 'discord': 'discord.com',
211
+ 'linear': 'linear.app',
212
+ 'asana': 'asana.com',
213
+ 'jira': 'atlassian.com',
214
+ 'confluence': 'atlassian.com',
215
+ 'trello': 'trello.com',
216
+ 'airtable': 'airtable.com',
217
+ 'coda': 'coda.io',
218
+ 'figma': 'figma.com',
219
+ 'miro': 'miro.com',
220
+ 'stripe': 'stripe.com',
221
+ 'paypal': 'paypal.com',
222
+ 'square': 'squareup.com',
223
+ 'shopify': 'shopify.com',
224
+ 'vercel': 'vercel.com',
225
+ 'netlify': 'netlify.com',
226
+ 'cloudflare': 'cloudflare.com',
227
+ 'aws': 'aws.amazon.com',
228
+ 'gcp': 'cloud.google.com',
229
+ 'azure': 'azure.microsoft.com',
230
+ 'supabase': 'supabase.com',
231
+ 'firebase': 'firebase.google.com',
232
+ 'mongodb': 'mongodb.com',
233
+ 'postgres': 'postgresql.org',
234
+ 'postgresql': 'postgresql.org',
235
+ 'mysql': 'mysql.com',
236
+ 'redis': 'redis.io',
237
+ 'elasticsearch': 'elastic.co',
238
+ 'docker': 'docker.com',
239
+ 'kubernetes': 'kubernetes.io',
240
+ 'sentry': 'sentry.io',
241
+ 'datadog': 'datadoghq.com',
242
+ 'grafana': 'grafana.com',
243
+ 'prometheus': 'prometheus.io',
244
+ 'openai': 'openai.com',
245
+ 'anthropic': 'anthropic.com',
246
+ 'huggingface': 'huggingface.co',
247
+ 'google': 'google.com',
248
+ 'brave': 'brave.com',
249
+ 'twitter': 'twitter.com',
250
+ 'x': 'x.com',
251
+ 'linkedin': 'linkedin.com',
252
+ 'facebook': 'facebook.com',
253
+ 'instagram': 'instagram.com',
254
+ 'youtube': 'youtube.com',
255
+ 'spotify': 'spotify.com',
256
+ 'twilio': 'twilio.com',
257
+ 'sendgrid': 'sendgrid.com',
258
+ 'mailchimp': 'mailchimp.com',
259
+ 'hubspot': 'hubspot.com',
260
+ 'salesforce': 'salesforce.com',
261
+ 'zendesk': 'zendesk.com',
262
+ 'intercom': 'intercom.com',
263
+ 'zapier': 'zapier.com',
264
+ 'ifttt': 'ifttt.com',
265
+ 'dropbox': 'dropbox.com',
266
+ 'box': 'box.com',
267
+ 'gdrive': 'drive.google.com',
268
+ 'onedrive': 'onedrive.live.com',
269
+ 'evernote': 'evernote.com',
270
+ 'todoist': 'todoist.com',
271
+ 'monday': 'monday.com',
272
+ 'clickup': 'clickup.com',
273
+ 'basecamp': 'basecamp.com',
274
+ 'npm': 'npmjs.com',
275
+ 'yarn': 'yarnpkg.com',
276
+ 'pnpm': 'pnpm.io',
277
+ 'pip': 'pypi.org',
278
+ 'python': 'python.org',
279
+ 'node': 'nodejs.org',
280
+ 'deno': 'deno.land',
281
+ 'rust': 'rust-lang.org',
282
+ 'go': 'go.dev',
283
+ 'java': 'java.com',
284
+ 'kotlin': 'kotlinlang.org',
285
+ 'swift': 'swift.org',
286
+ 'typescript': 'typescriptlang.org',
287
+ 'webpack': 'webpack.js.org',
288
+ 'vite': 'vitejs.dev',
289
+ 'esbuild': 'esbuild.github.io',
290
+ 'rollup': 'rollupjs.org',
291
+ 'babel': 'babeljs.io',
292
+ 'eslint': 'eslint.org',
293
+ 'prettier': 'prettier.io',
294
+ 'jest': 'jestjs.io',
295
+ 'mocha': 'mochajs.org',
296
+ 'cypress': 'cypress.io',
297
+ 'playwright': 'playwright.dev',
298
+ 'puppeteer': 'pptr.dev',
299
+ 'selenium': 'selenium.dev',
300
+ 'react': 'react.dev',
301
+ 'vue': 'vuejs.org',
302
+ 'angular': 'angular.io',
303
+ 'svelte': 'svelte.dev',
304
+ 'nextjs': 'nextjs.org',
305
+ 'nuxt': 'nuxt.com',
306
+ 'gatsby': 'gatsbyjs.com',
307
+ 'remix': 'remix.run',
308
+ 'astro': 'astro.build',
309
+ 'tailwind': 'tailwindcss.com',
310
+ 'bootstrap': 'getbootstrap.com',
311
+ 'material': 'material.io',
312
+ 'chakra': 'chakra-ui.com',
313
+ 'neon': 'neon.tech',
314
+ 'prisma': 'prisma.io',
315
+ 'drizzle': 'orm.drizzle.team',
316
+ 'sequelize': 'sequelize.org',
317
+ 'typeorm': 'typeorm.io',
318
+ };
319
+
320
+ if (domainMap[name]) {
321
+ return domainMap[name];
322
+ }
323
+
324
+ // Try common TLDs
325
+ if (name.length >= 3) {
326
+ return `${name}.com`;
327
+ }
328
+
329
+ return null;
330
+ }
331
+
332
+ /**
333
+ * Get initials for fallback display
334
+ */
335
+ export function getInitials(name) {
336
+ return name
337
+ .split(/[\s-_]+/)
338
+ .slice(0, 2)
339
+ .map(word => word.charAt(0).toUpperCase())
340
+ .join('');
341
+ }
342
+
343
+ /**
344
+ * Create logo HTML element with fallback
345
+ */
346
+ export function createLogoElement(logoUrl, name, size = 40) {
347
+ const initials = getInitials(name);
348
+
349
+ if (logoUrl) {
350
+ return `
351
+ <img src="${logoUrl}" alt="${name}" class="server-logo" style="width:${size}px;height:${size}px"
352
+ onerror="this.style.display='none'; this.nextElementSibling.style.display='flex'">
353
+ <div class="server-logo-fallback" style="display:none;width:${size}px;height:${size}px">${initials}</div>
354
+ `;
355
+ }
356
+
357
+ return `<div class="server-logo-fallback" style="width:${size}px;height:${size}px">${initials}</div>`;
358
+ }
359
+
360
+ /**
361
+ * Preload logos for a list of servers
362
+ */
363
+ export async function preloadLogos(servers) {
364
+ await loadStaticLogos();
365
+ const promises = servers.map(server =>
366
+ getServerLogo(server.name || server.id, server.config || server)
367
+ );
368
+ await Promise.all(promises);
369
+ }
370
+
371
+ /**
372
+ * Get logo URL for a domain using logo.dev
373
+ * Useful for custom icon lookup
374
+ * @param {string} domain - Domain like 'github.com'
375
+ * @param {object} options - Optional: size, format, grayscale
376
+ * @returns {string} Logo URL
377
+ */
378
+ export function getLogoByDomain(domain, options = {}) {
379
+ return getLogoDevUrl(domain, options);
380
+ }
381
+
382
+ /**
383
+ * Clear logo cache (useful when preferences change)
384
+ * @param {string} serverName - Optional: clear specific server, or all if not provided
385
+ */
386
+ export function clearLogoCache(serverName = null) {
387
+ if (serverName) {
388
+ logoCache.delete(serverName.toLowerCase());
389
+ } else {
390
+ logoCache.clear();
391
+ }
392
+ }
package/public/js/main.js CHANGED
@@ -2,7 +2,8 @@ import { detectClientsApi, listClientsApi } from './api.js';
2
2
  import { initClientView, renderClientList, selectClient, loadClientServers } from './clientView.js';
3
3
  import { initKanbanView, renderKanbanGrid } from './kanbanView.js';
4
4
  import { initServerView, renderAllServers } from './serverView.js';
5
- import { showServerModal, showImportModal, exportConfig, initModals, showRemoteServerModal } from './modals.js';
5
+ import { showServerModal, showImportModal, exportConfig, initModals, showRemoteServerModal, showSelectClientModal } from './modals.js';
6
+ import { showPopularMcpsModal, closePopularMcpsModal } from './popularMcps.js';
6
7
 
7
8
  document.addEventListener('DOMContentLoaded', () => {
8
9
  let currentClient = null;
@@ -101,6 +102,23 @@ document.addEventListener('DOMContentLoaded', () => {
101
102
  });
102
103
  document.getElementById('exportConfigBtn').addEventListener('click', exportConfig);
103
104
  document.getElementById('importConfigBtn').addEventListener('click', () => showImportModal(loadClientServers, renderKanbanGrid, window.loadClients));
105
+ document.getElementById('browsePopularMcpsBtn').addEventListener('click', () => {
106
+ showPopularMcpsModal(clients, window.loadClients);
107
+ });
108
+
109
+ // Kanban view add server button
110
+ document.getElementById('kanbanAddServerBtn').addEventListener('click', () => {
111
+ showSelectClientModal((selectedClientId) => {
112
+ showServerModal(null, null, loadClientServers, renderKanbanGrid, selectedClientId, window.loadClients);
113
+ }, window.loadClients);
114
+ });
115
+
116
+ // Server view add server button
117
+ document.getElementById('serverViewAddServerBtn').addEventListener('click', () => {
118
+ showSelectClientModal((selectedClientId) => {
119
+ showServerModal(null, null, renderAllServers, null, selectedClientId, window.loadClients);
120
+ }, window.loadClients);
121
+ });
104
122
 
105
123
  // Close modals when clicking outside
106
124
  document.querySelectorAll('.modal').forEach(modal => {
@@ -131,6 +149,8 @@ document.addEventListener('DOMContentLoaded', () => {
131
149
  document.getElementById('addServerToClientsModal').style.display = 'none';
132
150
  });
133
151
 
152
+ document.getElementById('closePopularMcps').addEventListener('click', closePopularMcpsModal);
153
+
134
154
  // Load clients on page load
135
155
  loadClients();
136
156
  });