imcp 0.0.18 → 0.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/.roo/rules-code/rules.md +88 -0
- package/dist/cli/index.js +0 -0
- package/dist/core/metadatas/constants.d.ts +7 -0
- package/dist/core/metadatas/constants.js +7 -0
- package/dist/core/onboard/FeedOnboardService.d.ts +7 -3
- package/dist/core/onboard/FeedOnboardService.js +52 -5
- package/dist/core/onboard/OnboardProcessor.js +22 -22
- package/dist/services/MCPManager.js +66 -6
- package/dist/services/TelemetryService.d.ts +15 -0
- package/dist/services/TelemetryService.js +54 -0
- package/dist/utils/githubAuth.js +65 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +78 -1
- package/dist/utils/versionUtils.d.ts +1 -0
- package/dist/utils/versionUtils.js +29 -0
- package/dist/web/public/css/serverCategoryList.css +120 -0
- package/dist/web/public/index.html +6 -3
- package/dist/web/public/js/flights/flights.js +0 -1
- package/dist/web/public/js/onboard/formProcessor.js +18 -11
- package/dist/web/public/js/onboard/publishHandler.js +30 -0
- package/dist/web/public/js/onboard/templates.js +5 -1
- package/dist/web/public/js/onboard/uiHandlers.js +266 -39
- package/dist/web/public/js/onboard/validationHandlers.js +71 -39
- package/dist/web/public/js/serverCategoryList.js +91 -7
- package/dist/web/public/onboard.html +2 -2
- package/dist/web/server.js +11 -1
- package/{src/web/public/js/onboard → docs}/ONBOARDING_PAGE_DESIGN.md +15 -125
- package/docs/Telemetry.md +136 -0
- package/memory-bank/activeContext.md +14 -0
- package/memory-bank/decisionLog.md +28 -0
- package/memory-bank/productContext.md +41 -0
- package/memory-bank/progress.md +5 -0
- package/memory-bank/systemPatterns.md +3 -0
- package/package.json +2 -1
- package/src/core/metadatas/constants.ts +9 -0
- package/src/core/onboard/FeedOnboardService.ts +59 -5
- package/src/core/onboard/OnboardProcessor.ts +25 -23
- package/src/services/MCPManager.ts +78 -8
- package/src/services/TelemetryService.ts +59 -0
- package/src/utils/githubAuth.ts +84 -1
- package/src/utils/logger.ts +83 -1
- package/src/utils/versionUtils.ts +33 -0
- package/src/web/public/css/serverCategoryList.css +120 -0
- package/src/web/public/index.html +6 -3
- package/src/web/public/js/onboard/formProcessor.js +18 -11
- package/src/web/public/js/onboard/publishHandler.js +30 -0
- package/src/web/public/js/onboard/templates.js +5 -1
- package/src/web/public/js/onboard/uiHandlers.js +266 -39
- package/src/web/public/js/onboard/validationHandlers.js +71 -39
- package/src/web/public/js/serverCategoryList.js +91 -7
- package/src/web/public/onboard.html +2 -2
- package/src/web/server.ts +11 -1
- package/dist/cli/commands/start.d.ts +0 -2
- package/dist/cli/commands/start.js +0 -32
- package/dist/cli/commands/sync.d.ts +0 -2
- package/dist/cli/commands/sync.js +0 -17
- package/dist/core/ConfigurationLoader.d.ts +0 -32
- package/dist/core/ConfigurationLoader.js +0 -236
- package/dist/core/ConfigurationProvider.d.ts +0 -35
- package/dist/core/ConfigurationProvider.js +0 -375
- package/dist/core/InstallationService.d.ts +0 -50
- package/dist/core/InstallationService.js +0 -350
- package/dist/core/MCPManager.d.ts +0 -28
- package/dist/core/MCPManager.js +0 -188
- package/dist/core/RequirementService.d.ts +0 -40
- package/dist/core/RequirementService.js +0 -110
- package/dist/core/ServerSchemaLoader.d.ts +0 -11
- package/dist/core/ServerSchemaLoader.js +0 -43
- package/dist/core/ServerSchemaProvider.d.ts +0 -17
- package/dist/core/ServerSchemaProvider.js +0 -120
- package/dist/core/constants.d.ts +0 -47
- package/dist/core/constants.js +0 -94
- package/dist/core/installers/BaseInstaller.d.ts +0 -74
- package/dist/core/installers/BaseInstaller.js +0 -253
- package/dist/core/installers/ClientInstaller.d.ts +0 -23
- package/dist/core/installers/ClientInstaller.js +0 -564
- package/dist/core/installers/CommandInstaller.d.ts +0 -37
- package/dist/core/installers/CommandInstaller.js +0 -173
- package/dist/core/installers/GeneralInstaller.d.ts +0 -33
- package/dist/core/installers/GeneralInstaller.js +0 -85
- package/dist/core/installers/InstallerFactory.d.ts +0 -54
- package/dist/core/installers/InstallerFactory.js +0 -97
- package/dist/core/installers/NpmInstaller.d.ts +0 -26
- package/dist/core/installers/NpmInstaller.js +0 -127
- package/dist/core/installers/PipInstaller.d.ts +0 -28
- package/dist/core/installers/PipInstaller.js +0 -127
- package/dist/core/installers/RequirementInstaller.d.ts +0 -33
- package/dist/core/installers/RequirementInstaller.js +0 -3
- package/dist/core/types.d.ts +0 -166
- package/dist/core/types.js +0 -16
- package/dist/services/InstallRequestValidator.d.ts +0 -21
- package/dist/services/InstallRequestValidator.js +0 -99
- package/dist/web/public/js/modal/installHandler.js +0 -227
- package/dist/web/public/js/modal/loadingUI.js +0 -74
- package/dist/web/public/js/modal/modalUI.js +0 -214
- package/dist/web/public/js/modal/version.js +0 -20
|
@@ -10,17 +10,7 @@ function setupRealTimeValidation(formId) {
|
|
|
10
10
|
const form = document.getElementById(formId);
|
|
11
11
|
if (!form) return;
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
const showValidationMessage = (element, message, isError = true) => {
|
|
15
|
-
let messageDiv = element.nextElementSibling;
|
|
16
|
-
if (!messageDiv || !messageDiv.classList.contains('validation-message')) {
|
|
17
|
-
messageDiv = document.createElement('div');
|
|
18
|
-
messageDiv.className = `validation-message text-xs mt-1 ${isError ? 'text-red-500' : 'text-green-500'}`;
|
|
19
|
-
element.insertAdjacentElement('afterend', messageDiv);
|
|
20
|
-
}
|
|
21
|
-
messageDiv.textContent = message;
|
|
22
|
-
messageDiv.className = `validation-message text-xs mt-1 ${isError ? 'text-red-500' : 'text-green-500'}`;
|
|
23
|
-
};
|
|
13
|
+
if (!form) return;
|
|
24
14
|
|
|
25
15
|
// Name input validation (category or server)
|
|
26
16
|
const nameInput = form.querySelector('input[name="name"]');
|
|
@@ -1026,5 +1016,22 @@ export function resetOnboardFormDynamicContent(formId = 'onboardForm', serversLi
|
|
|
1026
1016
|
setupRealTimeValidation(formId);
|
|
1027
1017
|
}
|
|
1028
1018
|
|
|
1019
|
+
/**
|
|
1020
|
+
* Shows validation message under an input field
|
|
1021
|
+
* @param {HTMLElement} element - The element to show validation message for
|
|
1022
|
+
* @param {string} message - The validation message to display
|
|
1023
|
+
* @param {boolean} isError - Whether this is an error message
|
|
1024
|
+
*/
|
|
1025
|
+
export function showValidationMessage(element, message, isError = true) {
|
|
1026
|
+
let messageDiv = element.nextElementSibling;
|
|
1027
|
+
if (!messageDiv || !messageDiv.classList.contains('validation-message')) {
|
|
1028
|
+
messageDiv = document.createElement('div');
|
|
1029
|
+
messageDiv.className = `validation-message text-xs mt-1 ${isError ? 'text-red-500' : 'text-green-500'}`;
|
|
1030
|
+
element.insertAdjacentElement('afterend', messageDiv);
|
|
1031
|
+
}
|
|
1032
|
+
messageDiv.textContent = message;
|
|
1033
|
+
messageDiv.className = `validation-message text-xs mt-1 ${isError ? 'text-red-500' : 'text-green-500'}`;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1029
1036
|
// Export setupRealTimeValidation for external use if needed
|
|
1030
1037
|
export { setupRealTimeValidation };
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { showToast } from '../notifications.js';
|
|
3
3
|
import { getFormData } from './formProcessor.js';
|
|
4
4
|
import {
|
|
5
|
+
validateFormFields,
|
|
5
6
|
pollOperationStatus,
|
|
6
7
|
updateOperationDisplay,
|
|
7
8
|
getElementIdsByTab,
|
|
@@ -37,6 +38,35 @@ export async function handlePublish(event, activeTab, currentSelectedCategoryDat
|
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// Validate form fields
|
|
42
|
+
const validationResult = validateFormFields(onboardForm, activeTab);
|
|
43
|
+
if (!validationResult.isValid) {
|
|
44
|
+
showToast('Please fix all validation errors before proceeding.', 'error');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (activeTab === 'create-category') {
|
|
49
|
+
const formData = getFormData(onboardForm, false);
|
|
50
|
+
if (!formData.mcpServers || formData.mcpServers.length === 0) {
|
|
51
|
+
showToast('At least one MCP server must be configured for a new category.', 'error');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check category name format
|
|
56
|
+
const nameInput = onboardForm.querySelector('input[name="name"]');
|
|
57
|
+
if (nameInput && nameInput.value.trim()) {
|
|
58
|
+
if (!/^[a-zA-Z0-9-_]+$/.test(nameInput.value.trim())) {
|
|
59
|
+
showValidationMessage(nameInput, 'Only alphanumeric characters, hyphens, and underscores allowed', true);
|
|
60
|
+
hasErrors = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (hasErrors) {
|
|
66
|
+
showToast('Please fix all validation errors before proceeding.', 'error');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
40
70
|
publishButton.disabled = true;
|
|
41
71
|
publishButton.innerHTML = "<i class='bx bx-loader-alt bx-spin mr-2'></i>Publishing...";
|
|
42
72
|
validateButton.disabled = true; // Also disable validate button during publish
|
|
@@ -17,7 +17,7 @@ export const serverTemplate = (serverIndex, isReadOnly = false, serverData = nul
|
|
|
17
17
|
|
|
18
18
|
const disabledAttr = makeServerFullyEditable ? '' : 'disabled';
|
|
19
19
|
const readOnlyClasses = makeServerFullyEditable ? '' : 'bg-gray-100 cursor-not-allowed opacity-70';
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
// "Remove Server" button should be hidden if the server is NOT fully editable.
|
|
22
22
|
// A server is fully editable if the context isn't read-only OR if it's an adhoc server.
|
|
23
23
|
const hideRemoveServerButtonClass = !makeServerFullyEditable ? 'hidden' : '';
|
|
@@ -42,6 +42,10 @@ export const serverTemplate = (serverIndex, isReadOnly = false, serverData = nul
|
|
|
42
42
|
class="action-button-in-server p-1.5 text-sm text-red-600 hover:text-red-800 hover:bg-red-50 rounded-md flex items-center mr-2 ${hideRemoveServerButtonClass}" title="Remove Server">
|
|
43
43
|
<i class='bx bx-trash text-lg'></i>
|
|
44
44
|
</button>
|
|
45
|
+
<button type="button" onclick="event.stopPropagation(); window.duplicateServer(${serverIndex}, '${serversListId}')"
|
|
46
|
+
class="action-button-in-server duplicate-mcp-server-button p-1.5 text-sm text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-md flex items-center mr-2" title="Duplicate Server">
|
|
47
|
+
<i class='bx bx-copy text-lg'></i>
|
|
48
|
+
</button>
|
|
45
49
|
<i class='bx bxs-chevron-down text-xl toggle-icon'></i>
|
|
46
50
|
</div>
|
|
47
51
|
</div>
|
|
@@ -70,29 +70,29 @@ function reindexServers(serversListId = 'serversList') {
|
|
|
70
70
|
// Ensure the server header toggle onclick is correctly re-indexed for its ID parameters
|
|
71
71
|
const headerToggle = serverItem.querySelector(`#${serversListId}-server-header-${oldServerIndex}`);
|
|
72
72
|
if (headerToggle) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
73
|
+
headerToggle.id = `${serversListId}-server-header-${newServerIndex}`;
|
|
74
|
+
const oldContentId = `${serversListId}-server-content-${oldServerIndex}`;
|
|
75
|
+
const newContentId = `${serversListId}-server-content-${newServerIndex}`;
|
|
76
|
+
let onclickAttr = headerToggle.getAttribute('onclick');
|
|
77
|
+
if (onclickAttr) {
|
|
78
|
+
onclickAttr = onclickAttr.replace(new RegExp(oldContentId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newContentId);
|
|
79
|
+
headerToggle.setAttribute('onclick', onclickAttr);
|
|
80
|
+
}
|
|
81
|
+
let onkeydownAttr = headerToggle.getAttribute('onkeydown');
|
|
82
|
+
if (onkeydownAttr) {
|
|
83
|
+
onkeydownAttr = onkeydownAttr.replace(new RegExp(oldContentId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newContentId);
|
|
84
|
+
headerToggle.setAttribute('onkeydown', onkeydownAttr);
|
|
85
|
+
}
|
|
86
|
+
headerToggle.setAttribute('aria-controls', newContentId);
|
|
87
87
|
}
|
|
88
88
|
// Update title ID for aria-labelledby
|
|
89
89
|
const titleForAria = serverItem.querySelector(`#${serversListId}-server-title-${oldServerIndex}`);
|
|
90
90
|
if (titleForAria) {
|
|
91
|
-
|
|
91
|
+
titleForAria.id = `${serversListId}-server-title-${newServerIndex}`;
|
|
92
92
|
}
|
|
93
93
|
const contentRegion = serverItem.querySelector(`#${serversListId}-server-content-${oldServerIndex}`);
|
|
94
94
|
if (contentRegion) {
|
|
95
|
-
|
|
95
|
+
contentRegion.setAttribute('aria-labelledby', `${serversListId}-server-title-${newServerIndex}`);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
|
|
@@ -225,17 +225,24 @@ export function addServer(serversListId = 'serversList', isContextGenerallyReadO
|
|
|
225
225
|
|
|
226
226
|
const serverItem = container.querySelector(`.server-item[data-index="${newServerIndex}"]`);
|
|
227
227
|
if (serverItem) {
|
|
228
|
-
|
|
228
|
+
// Handle systemTags dataset
|
|
229
|
+
if (serverData && serverData.systemTags) {
|
|
229
230
|
serverItem.dataset.systemTags = JSON.stringify(serverData.systemTags);
|
|
230
|
-
} else if (serverData && !serverData.systemTags) {
|
|
231
|
-
// Ensure no stale adhoc tag if the DOM element was somehow reused and had it previously.
|
|
231
|
+
} else if (serverData && !serverData.systemTags) {
|
|
232
232
|
delete serverItem.dataset.systemTags;
|
|
233
|
-
} else if (!serverData) {
|
|
234
|
-
// A brand new server is NOT adhoc by default. It becomes adhoc if modified (e.g. via JSON).
|
|
235
|
-
// This fulfills part of Req 3: newly added one in create-server tab should not have systemTags.
|
|
236
|
-
// And implicitly part of Req 2: new server in create-category tab also won't have systemTags.
|
|
233
|
+
} else if (!serverData) {
|
|
237
234
|
delete serverItem.dataset.systemTags;
|
|
238
235
|
}
|
|
236
|
+
|
|
237
|
+
// Handle originalName dataset
|
|
238
|
+
if (serverData && serverData.name) {
|
|
239
|
+
serverItem.dataset.originalName = serverData.name;
|
|
240
|
+
} else {
|
|
241
|
+
// If it's a new server (serverData is null) or serverData has no name,
|
|
242
|
+
// ensure no stale originalName if the DOM element was somehow reused.
|
|
243
|
+
delete serverItem.dataset.originalName;
|
|
244
|
+
}
|
|
245
|
+
|
|
239
246
|
setupServerMode(serverItem, newServerIndex, serversListId, actualReadOnlyForThisServer, serverData);
|
|
240
247
|
// Pass serverData to setupReadOnlyState so it can accurately determine adhoc status for title.
|
|
241
248
|
setupReadOnlyState(serverItem, actualReadOnlyForThisServer, serverData, serversListId, newServerIndex);
|
|
@@ -334,11 +341,11 @@ function setupReadOnlyState(serverItem, isEffectivelyReadOnly, serverDataFromPop
|
|
|
334
341
|
// If it's somehow missing and it's adhoc, ensure it's present.
|
|
335
342
|
// This situation should be rare if template logic is correct.
|
|
336
343
|
if (!titleElement.querySelector('span.text-blue-600')) {
|
|
337
|
-
|
|
344
|
+
titleElement.innerHTML = `${baseTitle} <span class="text-sm text-blue-600 ml-1">(Adhoc - Editable)</span>`;
|
|
338
345
|
} else {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
346
|
+
// Ensure base title is correct if adhoc span is already there
|
|
347
|
+
const adhocSpanHTML = titleElement.querySelector('span.text-blue-600').outerHTML;
|
|
348
|
+
titleElement.innerHTML = `${baseTitle} ${adhocSpanHTML}`;
|
|
342
349
|
}
|
|
343
350
|
}
|
|
344
351
|
}
|
|
@@ -353,10 +360,20 @@ function setupReadOnlyState(serverItem, isEffectivelyReadOnly, serverDataFromPop
|
|
|
353
360
|
}
|
|
354
361
|
});
|
|
355
362
|
|
|
356
|
-
// Hide
|
|
363
|
+
// Hide action buttons (Add Dependency, Add Env Var, Remove Server) for this server if it's read-only,
|
|
364
|
+
// but ensure the "Duplicate Server" button remains visible.
|
|
357
365
|
serverItem.querySelectorAll('.action-button-in-server').forEach(btn => {
|
|
366
|
+
if (!btn.classList.contains('duplicate-mcp-server-button')) {
|
|
367
|
+
// This is NOT the duplicate button, so hide it if the server is read-only.
|
|
358
368
|
btn.style.display = 'none';
|
|
359
369
|
btn.classList.add('hidden');
|
|
370
|
+
} else {
|
|
371
|
+
// This IS the duplicate button. Ensure it's visible.
|
|
372
|
+
// (It should be visible by default as it doesn't have conditional hide classes from the template)
|
|
373
|
+
btn.style.display = 'flex'; // Or its appropriate display style
|
|
374
|
+
btn.classList.remove('hidden');
|
|
375
|
+
btn.disabled = false; // Ensure it's enabled
|
|
376
|
+
}
|
|
360
377
|
});
|
|
361
378
|
|
|
362
379
|
// Expand server content and all key sections
|
|
@@ -385,11 +402,11 @@ function setupReadOnlyState(serverItem, isEffectivelyReadOnly, serverDataFromPop
|
|
|
385
402
|
// Check if the adhoc span is already there from the template.
|
|
386
403
|
// If not (e.g. server became adhoc after initial render and this is a re-evaluation), add it.
|
|
387
404
|
if (!titleElement.querySelector('span.text-blue-600')) {
|
|
388
|
-
|
|
405
|
+
titleElement.innerHTML = `${baseTitle} <span class="text-sm text-blue-600 ml-1">(Adhoc - Editable)</span>`;
|
|
389
406
|
} else {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
407
|
+
// Ensure base title is correct if adhoc span is already there
|
|
408
|
+
const adhocSpanHTML = titleElement.querySelector('span.text-blue-600').outerHTML;
|
|
409
|
+
titleElement.innerHTML = `${baseTitle} ${adhocSpanHTML}`;
|
|
393
410
|
}
|
|
394
411
|
} else {
|
|
395
412
|
// Editable and not adhoc, just the base title.
|
|
@@ -399,19 +416,19 @@ function setupReadOnlyState(serverItem, isEffectivelyReadOnly, serverDataFromPop
|
|
|
399
416
|
setTimeout(() => {
|
|
400
417
|
// Enable main server fields
|
|
401
418
|
serverItem.querySelectorAll('input, select, textarea').forEach(el => {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
419
|
+
if (!el.closest('.env-var-item') && !el.closest('.server-requirement-item')) {
|
|
420
|
+
el.disabled = false;
|
|
421
|
+
el.removeAttribute('readonly'); // Ensure readonly is also removed
|
|
422
|
+
el.classList.remove('bg-gray-100', 'cursor-not-allowed', 'opacity-70');
|
|
406
423
|
}
|
|
407
424
|
});
|
|
408
425
|
|
|
409
426
|
// Show ALL action buttons for this server (Add Dependency, Add Env Var, Remove Server)
|
|
410
427
|
// as the server is determined to be effectively editable.
|
|
411
428
|
serverItem.querySelectorAll('.action-button-in-server').forEach(btn => {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
429
|
+
btn.style.display = 'flex'; // Or 'inline-flex' or whatever its default visible display is
|
|
430
|
+
btn.disabled = false;
|
|
431
|
+
btn.classList.remove('hidden', 'opacity-50', 'cursor-not-allowed');
|
|
415
432
|
});
|
|
416
433
|
}, 0);
|
|
417
434
|
}
|
|
@@ -832,6 +849,216 @@ export async function copyJsonToClipboard() {
|
|
|
832
849
|
}
|
|
833
850
|
}
|
|
834
851
|
|
|
852
|
+
/**
|
|
853
|
+
* Duplicates an MCP server item in the form.
|
|
854
|
+
* @param {number} serverIndexToDuplicate - The index of the server to duplicate.
|
|
855
|
+
* @param {string} serversListId - The ID of the servers list container (e.g., 'serversList', 'existingCategoryServersList').
|
|
856
|
+
*/
|
|
857
|
+
export function duplicateServer(serverIndexToDuplicate, serversListId) {
|
|
858
|
+
const formId = serversListId === 'serversList' ? 'onboardForm' : 'onboardServerForm';
|
|
859
|
+
const formElement = document.getElementById(formId);
|
|
860
|
+
if (!formElement) {
|
|
861
|
+
showToast(`Form with ID ${formId} not found. Cannot duplicate server.`, 'error');
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Determine if we are in the "Create Server in Existing Category" tab context
|
|
866
|
+
const isExistingCategoryContext = serversListId === 'existingCategoryServersList';
|
|
867
|
+
let baseCategoryDataForFormExtraction = null;
|
|
868
|
+
if (isExistingCategoryContext) {
|
|
869
|
+
// If in existing category context, getFormData might need the base category data
|
|
870
|
+
// This depends on how getFormData is structured to handle this tab.
|
|
871
|
+
// Assuming window.currentSelectedCategoryData holds the loaded category.
|
|
872
|
+
baseCategoryDataForFormExtraction = window.currentSelectedCategoryData || null;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const serverItemToDuplicate = formElement.querySelector(`.server-item[data-index="${serverIndexToDuplicate}"]`);
|
|
876
|
+
if (!serverItemToDuplicate) {
|
|
877
|
+
showToast('Could not find the server item to duplicate in the DOM.', 'error');
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Track and temporarily enable ALL disabled fields in the form to ensure we get complete data
|
|
882
|
+
const disabledFields = [];
|
|
883
|
+
formElement.querySelectorAll('input, select, textarea').forEach(el => {
|
|
884
|
+
if (el.disabled) {
|
|
885
|
+
disabledFields.push({element: el, wasDisabled: true});
|
|
886
|
+
el.disabled = false;
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
// Get all current server data from the form
|
|
891
|
+
const currentFullFeedConfig = getFormData(formElement, isExistingCategoryContext, baseCategoryDataForFormExtraction);
|
|
892
|
+
|
|
893
|
+
// Restore original disabled state for all fields
|
|
894
|
+
disabledFields.forEach(({element, wasDisabled}) => {
|
|
895
|
+
element.disabled = wasDisabled;
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
if (!currentFullFeedConfig || !currentFullFeedConfig.mcpServers ||
|
|
899
|
+
currentFullFeedConfig.mcpServers.length <= serverIndexToDuplicate) {
|
|
900
|
+
showToast('Could not retrieve data for the server to duplicate after attempting to enable fields.', 'error');
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// 2. Get the specific server data to duplicate (deep copy)
|
|
905
|
+
let serverToDuplicateData = JSON.parse(JSON.stringify(currentFullFeedConfig.mcpServers[serverIndexToDuplicate]));
|
|
906
|
+
|
|
907
|
+
// 3. IMPORTANT: Remove systemTags from the duplicated data.
|
|
908
|
+
// The duplicated server should be treated as a new, fully editable server.
|
|
909
|
+
if (serverToDuplicateData.systemTags) {
|
|
910
|
+
delete serverToDuplicateData.systemTags;
|
|
911
|
+
}
|
|
912
|
+
// Also, ensure its name is distinct if needed, or clear it to force user input.
|
|
913
|
+
// For now, we'll copy the name but it will be editable.
|
|
914
|
+
// Consider adding a suffix like "-copy" to the name if automatic distinct names are desired.
|
|
915
|
+
// serverToDuplicateData.name = serverToDuplicateData.name ? `${serverToDuplicateData.name}-copy` : 'duplicated-server';
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
// 4. Determine if the general context for adding a new server is read-only
|
|
919
|
+
// This is primarily for the 'Edit Existing Category' tab.
|
|
920
|
+
// 'Create New Category' tab is never read-only in this sense.
|
|
921
|
+
let isContextGenerallyReadOnlyForAddServer = false;
|
|
922
|
+
if (isExistingCategoryContext) {
|
|
923
|
+
isContextGenerallyReadOnlyForAddServer = window.isExistingCategoryReadOnly || false;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// 5. Add a new server item. Pass `null` for serverData to ensure it's treated as a new item.
|
|
927
|
+
// The `isContextGenerallyReadOnlyForAddServer` helps `addServer` decide initial template state.
|
|
928
|
+
const newServerIndex = addServer(serversListId, isContextGenerallyReadOnlyForAddServer, null);
|
|
929
|
+
|
|
930
|
+
if (newServerIndex === -1) {
|
|
931
|
+
showToast('Failed to add a new server item for duplication.', 'error');
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// 6. Populate the new server item with the copied data.
|
|
936
|
+
// The new server should be fully editable.
|
|
937
|
+
// We need to call populateForm or a similar function for just this new server.
|
|
938
|
+
// populateForm expects a full FeedConfiguration. We'll construct a minimal one.
|
|
939
|
+
const tempFeedConfigForPopulation = {
|
|
940
|
+
mcpServers: [serverToDuplicateData] // Contains only the server data to populate
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
// Call populateForm, but ensure it targets the *new* server index and makes it editable.
|
|
944
|
+
// The populateForm function needs to be aware it's populating a *specific, new* server.
|
|
945
|
+
// The last `true` tells populateForm to make this specific server editable, overriding general read-only context.
|
|
946
|
+
// This might require adjustments in populateForm or a dedicated populateSingleServerForm function.
|
|
947
|
+
// For now, assuming populateForm can handle this if we pass a single server and target the new index.
|
|
948
|
+
// We pass `false` for `renderServersAsReadOnly` to ensure the duplicated server is editable.
|
|
949
|
+
// And we pass `true` for a hypothetical `forceEditableForSingleServer` if populateForm supported it.
|
|
950
|
+
// Let's simplify: populateForm will populate based on the data. addServer already set it up as editable.
|
|
951
|
+
// We need to ensure that `populateForm` correctly populates the server at `newServerIndex`.
|
|
952
|
+
// The `populateForm` function in formProcessor.js needs to be able to populate a *specific* server
|
|
953
|
+
// if we pass only one server in `mcpServers`.
|
|
954
|
+
|
|
955
|
+
// Get the newly added server item
|
|
956
|
+
const serversList = document.getElementById(serversListId);
|
|
957
|
+
const newServerItem = serversList.querySelector(`.server-item[data-index="${newServerIndex}"]`);
|
|
958
|
+
|
|
959
|
+
if (newServerItem) {
|
|
960
|
+
// Directly populate fields for the new server.
|
|
961
|
+
// This is a simplified version of what populateForm does for a single server.
|
|
962
|
+
// This avoids needing to modify populateForm extensively for this specific use case.
|
|
963
|
+
populateServerManually(newServerItem, newServerIndex, serverToDuplicateData, serversListId);
|
|
964
|
+
showToast(`Server #${serverIndexToDuplicate + 1} duplicated to Server #${newServerIndex + 1}.`, 'success');
|
|
965
|
+
} else {
|
|
966
|
+
showToast('Failed to find the new server item after duplication.', 'error');
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
window.duplicateServer = duplicateServer;
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Manually populates a single server item's form fields with provided data.
|
|
973
|
+
* This is a helper for the duplicateServer functionality.
|
|
974
|
+
* @param {HTMLElement} serverItemElement - The HTML element of the server item.
|
|
975
|
+
* @param {number} serverIndex - The index of the server item.
|
|
976
|
+
* @param {object} serverData - The data object for the server.
|
|
977
|
+
* @param {string} serversListId - The ID of the servers list.
|
|
978
|
+
*/
|
|
979
|
+
function populateServerManually(serverItemElement, serverIndex, serverData, serversListId) {
|
|
980
|
+
// Populate basic fields
|
|
981
|
+
serverItemElement.querySelector(`input[name="servers[${serverIndex}].name"]`).value = serverData.name || '';
|
|
982
|
+
serverItemElement.querySelector(`textarea[name="servers[${serverIndex}].description"]`).value = serverData.description || '';
|
|
983
|
+
serverItemElement.querySelector(`select[name="servers[${serverIndex}].mode"]`).value = serverData.mode || 'stdio';
|
|
984
|
+
serverItemElement.querySelector(`input[name="servers[${serverIndex}].schemas"]`).value = serverData.schemas || '';
|
|
985
|
+
serverItemElement.querySelector(`input[name="servers[${serverIndex}].repository"]`).value = serverData.repository || '';
|
|
986
|
+
|
|
987
|
+
// Trigger change on mode to render correct installation config
|
|
988
|
+
const modeSelect = serverItemElement.querySelector(`select[name="servers[${serverIndex}].mode"]`);
|
|
989
|
+
if (modeSelect) {
|
|
990
|
+
modeSelect.dispatchEvent(new Event('change')); // This will call renderInstallationConfig
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Populate installation config (command/args for stdio, url for sse)
|
|
994
|
+
// renderInstallationConfig should have been called by the modeSelect change event.
|
|
995
|
+
// Now, set the values based on serverData.installation
|
|
996
|
+
if (serverData.mode === 'stdio' && serverData.installation) {
|
|
997
|
+
const commandInput = serverItemElement.querySelector(`input[name="servers[${serverIndex}].installation.command"]`);
|
|
998
|
+
if (commandInput) commandInput.value = serverData.installation.command || '';
|
|
999
|
+
const argsInput = serverItemElement.querySelector(`input[name="servers[${serverIndex}].installation.args"]`);
|
|
1000
|
+
if (argsInput) argsInput.value = serverData.installation.args ? serverData.installation.args.join(', ') : '';
|
|
1001
|
+
} else if (serverData.mode === 'sse' && serverData.installation) {
|
|
1002
|
+
const urlInput = serverItemElement.querySelector(`input[name="servers[${serverIndex}].installation.url"]`);
|
|
1003
|
+
if (urlInput) urlInput.value = serverData.installation.url || '';
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Populate environment variables
|
|
1007
|
+
const envVarsContainer = serverItemElement.querySelector(`#envVarsContainer_${serverIndex}`);
|
|
1008
|
+
if (envVarsContainer && serverData.installation && serverData.installation.env) {
|
|
1009
|
+
Object.entries(serverData.installation.env).forEach(([name, envConfig]) => {
|
|
1010
|
+
const envVarIndex = addEnvVariable(serverIndex, serversListId, false); // Add as editable
|
|
1011
|
+
const envVarItem = envVarsContainer.querySelector(`.env-var-item[data-env-index="${envVarIndex}"]`);
|
|
1012
|
+
if (envVarItem) {
|
|
1013
|
+
envVarItem.querySelector(`input[name="servers[${serverIndex}].installation.env[${envVarIndex}].name"]`).value = name;
|
|
1014
|
+
envVarItem.querySelector(`input[name="servers[${serverIndex}].installation.env[${envVarIndex}].default"]`).value = envConfig.Default || '';
|
|
1015
|
+
envVarItem.querySelector(`input[name="servers[${serverIndex}].installation.env[${envVarIndex}].required"]`).checked = envConfig.Required || false;
|
|
1016
|
+
envVarItem.querySelector(`textarea[name="servers[${serverIndex}].installation.env[${envVarIndex}].description"]`).value = envConfig.Description || '';
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Populate server requirements (dependencies)
|
|
1022
|
+
const serverReqsContainer = serverItemElement.querySelector(`#server-requirements-list-${serverIndex}`);
|
|
1023
|
+
if (serverReqsContainer && serverData.dependencies && serverData.dependencies.requirements) {
|
|
1024
|
+
serverData.dependencies.requirements.forEach(req => {
|
|
1025
|
+
const reqIndex = addServerRequirement(serverIndex, serversListId, false); // Add as editable
|
|
1026
|
+
const reqItem = serverReqsContainer.querySelector(`.server-requirement-item[data-req-index="${reqIndex}"]`);
|
|
1027
|
+
if (reqItem) {
|
|
1028
|
+
reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].name"]`).value = req.name || '';
|
|
1029
|
+
reqItem.querySelector(`select[name="servers[${serverIndex}].requirements[${reqIndex}].type"]`).value = req.type || '';
|
|
1030
|
+
reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].version"]`).value = req.version || '';
|
|
1031
|
+
reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].order"]`).value = req.order || '';
|
|
1032
|
+
reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].alias"]`).value = req.alias || '';
|
|
1033
|
+
|
|
1034
|
+
const typeSelect = reqItem.querySelector(`select[name="servers[${serverIndex}].requirements[${reqIndex}].type"]`);
|
|
1035
|
+
if (typeSelect) typeSelect.dispatchEvent(new Event('change')); // To show/hide alias
|
|
1036
|
+
|
|
1037
|
+
const registryTypeSelect = reqItem.querySelector(`select[name="servers[${serverIndex}].requirements[${reqIndex}].registryType"]`);
|
|
1038
|
+
if (registryTypeSelect) {
|
|
1039
|
+
registryTypeSelect.value = req.registryType || 'public';
|
|
1040
|
+
registryTypeSelect.dispatchEvent(new Event('change')); // To show/hide specific registry configs
|
|
1041
|
+
|
|
1042
|
+
if (req.registry) {
|
|
1043
|
+
if (req.registryType === 'github' && req.registry.githubRelease) {
|
|
1044
|
+
reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].registry.githubRelease.repository"]`).value = req.registry.githubRelease.repository || '';
|
|
1045
|
+
reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].registry.githubRelease.assetsName"]`).value = req.registry.githubRelease.assetsName || '';
|
|
1046
|
+
reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].registry.githubRelease.assetName"]`).value = req.registry.githubRelease.assetName || '';
|
|
1047
|
+
} else if (req.registryType === 'artifacts' && req.registry.artifacts) {
|
|
1048
|
+
reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].registry.artifacts.registryName"]`).value = req.registry.artifacts.registryName || '';
|
|
1049
|
+
reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].registry.artifacts.registryUrl"]`).value = req.registry.artifacts.registryUrl || '';
|
|
1050
|
+
} else if (req.registryType === 'local' && req.registry.local) {
|
|
1051
|
+
reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].registry.local.localPath"]`).value = req.registry.local.localPath || '';
|
|
1052
|
+
reqItem.querySelector(`input[name="servers[${serverIndex}].requirements[${reqIndex}].registry.local.assetName"]`).value = req.registry.local.assetName || '';
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
// Ensure the duplicated server is fully editable by calling setupReadOnlyState with false
|
|
1060
|
+
setupReadOnlyState(serverItemElement, false, null, serversListId, serverIndex);
|
|
1061
|
+
}
|
|
835
1062
|
Object.entries({
|
|
836
1063
|
addServer,
|
|
837
1064
|
removeServer,
|
|
@@ -9,7 +9,7 @@ import { toggleSectionContent } from './uiHandlers.js'; // Assuming this is corr
|
|
|
9
9
|
* @param {string} message - The validation message to display.
|
|
10
10
|
* @param {boolean} isError - Whether this is an error message (true) or success message (false).
|
|
11
11
|
*/
|
|
12
|
-
function showValidationMessage(element, message, isError = true) {
|
|
12
|
+
export function showValidationMessage(element, message, isError = true) {
|
|
13
13
|
// Remove any existing validation message
|
|
14
14
|
const existingMessage = element.nextElementSibling;
|
|
15
15
|
if (existingMessage && existingMessage.classList.contains('validation-message')) {
|
|
@@ -34,6 +34,72 @@ function showValidationMessage(element, message, isError = true) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Common form field validation used by both validate and publish handlers
|
|
39
|
+
* @param {HTMLFormElement} form - The form to validate
|
|
40
|
+
* @param {string} activeTab - The active tab ('create-category' or 'create-server')
|
|
41
|
+
* @returns {{isValid: boolean, errors: string[]}} Validation result with error messages
|
|
42
|
+
*/
|
|
43
|
+
export function validateFormFields(form, activeTab) {
|
|
44
|
+
// Clear any existing validation messages
|
|
45
|
+
form.querySelectorAll('.validation-message').forEach(el => el.remove());
|
|
46
|
+
|
|
47
|
+
let hasErrors = false;
|
|
48
|
+
const errors = [];
|
|
49
|
+
|
|
50
|
+
// Check for empty required fields and show validation messages
|
|
51
|
+
const emptyRequiredFields = Array.from(form.querySelectorAll('[required]'));
|
|
52
|
+
emptyRequiredFields.forEach(field => {
|
|
53
|
+
if (!field.value.trim()) {
|
|
54
|
+
showValidationMessage(field, 'This field is required', true);
|
|
55
|
+
hasErrors = true;
|
|
56
|
+
errors.push(`${field.name || 'Field'} is required`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Check category name format if this is the create category tab
|
|
61
|
+
if (activeTab === 'create-category') {
|
|
62
|
+
const nameInput = form.querySelector('input[name="name"]');
|
|
63
|
+
if (nameInput && nameInput.value.trim()) {
|
|
64
|
+
if (!/^[a-zA-Z0-9-_]+$/.test(nameInput.value.trim())) {
|
|
65
|
+
showValidationMessage(nameInput, 'Only alphanumeric characters, hyphens, and underscores allowed', true);
|
|
66
|
+
hasErrors = true;
|
|
67
|
+
errors.push('Category name must only contain alphanumeric characters, hyphens, and underscores');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for other validation errors
|
|
73
|
+
const envNameFields = form.querySelectorAll('input[name$=".env\\[\\].name"]');
|
|
74
|
+
envNameFields.forEach(field => {
|
|
75
|
+
const value = field.value.trim();
|
|
76
|
+
if (value && !/^[A-Z_][A-Z0-9_]*$/.test(value)) {
|
|
77
|
+
showValidationMessage(field, 'Must be uppercase with only letters, numbers, and underscores', true);
|
|
78
|
+
hasErrors = true;
|
|
79
|
+
errors.push('Environment variable names must be uppercase with only letters, numbers, and underscores');
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const urlFields = form.querySelectorAll('input[name$=".url"]');
|
|
84
|
+
urlFields.forEach(field => {
|
|
85
|
+
const value = field.value.trim();
|
|
86
|
+
if (value) {
|
|
87
|
+
try {
|
|
88
|
+
new URL(value);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
showValidationMessage(field, 'Invalid URL format', true);
|
|
91
|
+
hasErrors = true;
|
|
92
|
+
errors.push('Invalid URL format');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
isValid: !hasErrors,
|
|
99
|
+
errors
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
37
103
|
const POLLING_INTERVAL = 3000; // 3 seconds
|
|
38
104
|
let pollingIntervalId = null;
|
|
39
105
|
|
|
@@ -278,45 +344,11 @@ export async function handleValidation(event, activeTab, currentSelectedCategory
|
|
|
278
344
|
return;
|
|
279
345
|
}
|
|
280
346
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// Check for empty required fields and show validation messages
|
|
285
|
-
const emptyRequiredFields = Array.from(onboardForm.querySelectorAll('[required]'));
|
|
286
|
-
let hasErrors = false;
|
|
287
|
-
|
|
288
|
-
emptyRequiredFields.forEach(field => {
|
|
289
|
-
if (!field.value.trim()) {
|
|
290
|
-
showValidationMessage(field, 'This field is required', true);
|
|
291
|
-
hasErrors = true;
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
// Check for other validation errors
|
|
296
|
-
const envNameFields = onboardForm.querySelectorAll('input[name$=".env\\[\\].name"]');
|
|
297
|
-
envNameFields.forEach(field => {
|
|
298
|
-
const value = field.value.trim();
|
|
299
|
-
if (value && !/^[A-Z_][A-Z0-9_]*$/.test(value)) {
|
|
300
|
-
showValidationMessage(field, 'Must be uppercase with only letters, numbers, and underscores', true);
|
|
301
|
-
hasErrors = true;
|
|
302
|
-
}
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
const urlFields = onboardForm.querySelectorAll('input[name$=".url"]');
|
|
306
|
-
urlFields.forEach(field => {
|
|
307
|
-
const value = field.value.trim();
|
|
308
|
-
if (value) {
|
|
309
|
-
try {
|
|
310
|
-
new URL(value);
|
|
311
|
-
} catch (e) {
|
|
312
|
-
showValidationMessage(field, 'Invalid URL format', true);
|
|
313
|
-
hasErrors = true;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
if (hasErrors) {
|
|
347
|
+
const validationResult = validateFormFields(onboardForm, activeTab);
|
|
348
|
+
if (!validationResult.isValid) {
|
|
319
349
|
showToast('Please fix all validation errors before proceeding.', 'error');
|
|
350
|
+
validateButton.disabled = false;
|
|
351
|
+
validateButton.innerHTML = "<i class='bx bx-check-shield mr-2'></i>Validate";
|
|
320
352
|
return;
|
|
321
353
|
}
|
|
322
354
|
|