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.
Files changed (96) hide show
  1. package/.roo/rules-code/rules.md +88 -0
  2. package/dist/cli/index.js +0 -0
  3. package/dist/core/metadatas/constants.d.ts +7 -0
  4. package/dist/core/metadatas/constants.js +7 -0
  5. package/dist/core/onboard/FeedOnboardService.d.ts +7 -3
  6. package/dist/core/onboard/FeedOnboardService.js +52 -5
  7. package/dist/core/onboard/OnboardProcessor.js +22 -22
  8. package/dist/services/MCPManager.js +66 -6
  9. package/dist/services/TelemetryService.d.ts +15 -0
  10. package/dist/services/TelemetryService.js +54 -0
  11. package/dist/utils/githubAuth.js +65 -0
  12. package/dist/utils/logger.d.ts +16 -0
  13. package/dist/utils/logger.js +78 -1
  14. package/dist/utils/versionUtils.d.ts +1 -0
  15. package/dist/utils/versionUtils.js +29 -0
  16. package/dist/web/public/css/serverCategoryList.css +120 -0
  17. package/dist/web/public/index.html +6 -3
  18. package/dist/web/public/js/flights/flights.js +0 -1
  19. package/dist/web/public/js/onboard/formProcessor.js +18 -11
  20. package/dist/web/public/js/onboard/publishHandler.js +30 -0
  21. package/dist/web/public/js/onboard/templates.js +5 -1
  22. package/dist/web/public/js/onboard/uiHandlers.js +266 -39
  23. package/dist/web/public/js/onboard/validationHandlers.js +71 -39
  24. package/dist/web/public/js/serverCategoryList.js +91 -7
  25. package/dist/web/public/onboard.html +2 -2
  26. package/dist/web/server.js +11 -1
  27. package/{src/web/public/js/onboard → docs}/ONBOARDING_PAGE_DESIGN.md +15 -125
  28. package/docs/Telemetry.md +136 -0
  29. package/memory-bank/activeContext.md +14 -0
  30. package/memory-bank/decisionLog.md +28 -0
  31. package/memory-bank/productContext.md +41 -0
  32. package/memory-bank/progress.md +5 -0
  33. package/memory-bank/systemPatterns.md +3 -0
  34. package/package.json +2 -1
  35. package/src/core/metadatas/constants.ts +9 -0
  36. package/src/core/onboard/FeedOnboardService.ts +59 -5
  37. package/src/core/onboard/OnboardProcessor.ts +25 -23
  38. package/src/services/MCPManager.ts +78 -8
  39. package/src/services/TelemetryService.ts +59 -0
  40. package/src/utils/githubAuth.ts +84 -1
  41. package/src/utils/logger.ts +83 -1
  42. package/src/utils/versionUtils.ts +33 -0
  43. package/src/web/public/css/serverCategoryList.css +120 -0
  44. package/src/web/public/index.html +6 -3
  45. package/src/web/public/js/onboard/formProcessor.js +18 -11
  46. package/src/web/public/js/onboard/publishHandler.js +30 -0
  47. package/src/web/public/js/onboard/templates.js +5 -1
  48. package/src/web/public/js/onboard/uiHandlers.js +266 -39
  49. package/src/web/public/js/onboard/validationHandlers.js +71 -39
  50. package/src/web/public/js/serverCategoryList.js +91 -7
  51. package/src/web/public/onboard.html +2 -2
  52. package/src/web/server.ts +11 -1
  53. package/dist/cli/commands/start.d.ts +0 -2
  54. package/dist/cli/commands/start.js +0 -32
  55. package/dist/cli/commands/sync.d.ts +0 -2
  56. package/dist/cli/commands/sync.js +0 -17
  57. package/dist/core/ConfigurationLoader.d.ts +0 -32
  58. package/dist/core/ConfigurationLoader.js +0 -236
  59. package/dist/core/ConfigurationProvider.d.ts +0 -35
  60. package/dist/core/ConfigurationProvider.js +0 -375
  61. package/dist/core/InstallationService.d.ts +0 -50
  62. package/dist/core/InstallationService.js +0 -350
  63. package/dist/core/MCPManager.d.ts +0 -28
  64. package/dist/core/MCPManager.js +0 -188
  65. package/dist/core/RequirementService.d.ts +0 -40
  66. package/dist/core/RequirementService.js +0 -110
  67. package/dist/core/ServerSchemaLoader.d.ts +0 -11
  68. package/dist/core/ServerSchemaLoader.js +0 -43
  69. package/dist/core/ServerSchemaProvider.d.ts +0 -17
  70. package/dist/core/ServerSchemaProvider.js +0 -120
  71. package/dist/core/constants.d.ts +0 -47
  72. package/dist/core/constants.js +0 -94
  73. package/dist/core/installers/BaseInstaller.d.ts +0 -74
  74. package/dist/core/installers/BaseInstaller.js +0 -253
  75. package/dist/core/installers/ClientInstaller.d.ts +0 -23
  76. package/dist/core/installers/ClientInstaller.js +0 -564
  77. package/dist/core/installers/CommandInstaller.d.ts +0 -37
  78. package/dist/core/installers/CommandInstaller.js +0 -173
  79. package/dist/core/installers/GeneralInstaller.d.ts +0 -33
  80. package/dist/core/installers/GeneralInstaller.js +0 -85
  81. package/dist/core/installers/InstallerFactory.d.ts +0 -54
  82. package/dist/core/installers/InstallerFactory.js +0 -97
  83. package/dist/core/installers/NpmInstaller.d.ts +0 -26
  84. package/dist/core/installers/NpmInstaller.js +0 -127
  85. package/dist/core/installers/PipInstaller.d.ts +0 -28
  86. package/dist/core/installers/PipInstaller.js +0 -127
  87. package/dist/core/installers/RequirementInstaller.d.ts +0 -33
  88. package/dist/core/installers/RequirementInstaller.js +0 -3
  89. package/dist/core/types.d.ts +0 -166
  90. package/dist/core/types.js +0 -16
  91. package/dist/services/InstallRequestValidator.d.ts +0 -21
  92. package/dist/services/InstallRequestValidator.js +0 -99
  93. package/dist/web/public/js/modal/installHandler.js +0 -227
  94. package/dist/web/public/js/modal/loadingUI.js +0 -74
  95. package/dist/web/public/js/modal/modalUI.js +0 -214
  96. 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
- // Helper function to show validation message
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
- 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);
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
- titleForAria.id = `${serversListId}-server-title-${newServerIndex}`;
91
+ titleForAria.id = `${serversListId}-server-title-${newServerIndex}`;
92
92
  }
93
93
  const contentRegion = serverItem.querySelector(`#${serversListId}-server-content-${oldServerIndex}`);
94
94
  if (contentRegion) {
95
- contentRegion.setAttribute('aria-labelledby', `${serversListId}-server-title-${newServerIndex}`);
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
- if (serverData && serverData.systemTags) { // Case 1: Populating from existing serverData that has systemTags
228
+ // Handle systemTags dataset
229
+ if (serverData && serverData.systemTags) {
229
230
  serverItem.dataset.systemTags = JSON.stringify(serverData.systemTags);
230
- } else if (serverData && !serverData.systemTags) { // Case 3: Populating from serverData that *lacks* 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) { // Case 2: Adding a brand new server via "Add Server" button click (serverData is null)
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
- titleElement.innerHTML = `${baseTitle} <span class="text-sm text-blue-600 ml-1">(Adhoc - Editable)</span>`;
344
+ titleElement.innerHTML = `${baseTitle} <span class="text-sm text-blue-600 ml-1">(Adhoc - Editable)</span>`;
338
345
  } else {
339
- // Ensure base title is correct if adhoc span is already there
340
- const adhocSpanHTML = titleElement.querySelector('span.text-blue-600').outerHTML;
341
- titleElement.innerHTML = `${baseTitle} ${adhocSpanHTML}`;
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 all action buttons (Add Dependency, Add Env Var, Remove Server) for this server if it's read-only.
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
- titleElement.innerHTML = `${baseTitle} <span class="text-sm text-blue-600 ml-1">(Adhoc - Editable)</span>`;
405
+ titleElement.innerHTML = `${baseTitle} <span class="text-sm text-blue-600 ml-1">(Adhoc - Editable)</span>`;
389
406
  } else {
390
- // Ensure base title is correct if adhoc span is already there
391
- const adhocSpanHTML = titleElement.querySelector('span.text-blue-600').outerHTML;
392
- titleElement.innerHTML = `${baseTitle} ${adhocSpanHTML}`;
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
- if (!el.closest('.env-var-item') && !el.closest('.server-requirement-item')) {
403
- el.disabled = false;
404
- el.removeAttribute('readonly'); // Ensure readonly is also removed
405
- el.classList.remove('bg-gray-100', 'cursor-not-allowed', 'opacity-70');
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
- btn.style.display = 'flex'; // Or 'inline-flex' or whatever its default visible display is
413
- btn.disabled = false;
414
- btn.classList.remove('hidden', 'opacity-50', 'cursor-not-allowed');
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
- // First clear any existing validation messages
282
- onboardForm.querySelectorAll('.validation-message').forEach(el => el.remove());
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