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.
- package/README.md +81 -5
- package/package.json +1 -1
- package/public/index.html +104 -19
- package/public/js/clientView.js +53 -19
- package/public/js/iconPreferences.js +98 -0
- package/public/js/kanbanView.js +25 -42
- package/public/js/logoService.js +392 -0
- package/public/js/main.js +21 -1
- package/public/js/modals.js +344 -24
- package/public/js/popularMcps.js +304 -0
- package/public/js/serverView.js +24 -7
- package/public/mcp-logos.json +26 -0
- package/public/popular-mcps.json +555 -0
- package/public/style.css +617 -1
- package/src/config-manager.js +3 -5
|
@@ -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, "'")}'>
|
|
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(/'/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
|
+
}
|
package/public/js/serverView.js
CHANGED
|
@@ -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';
|
|
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.
|
|
103
|
-
const serverConfig = JSON.parse(e.
|
|
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.
|
|
111
|
-
const serverConfig = JSON.parse(e.
|
|
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.
|
|
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.
|
|
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
|
}
|
package/public/mcp-logos.json
CHANGED
|
@@ -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",
|