imcp 0.0.16 → 0.0.17

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 (135) hide show
  1. package/dist/cli/commands/install.js +2 -2
  2. package/dist/cli/commands/list.js +2 -2
  3. package/dist/cli/commands/serve.js +1 -1
  4. package/dist/core/RequirementService.d.ts +0 -12
  5. package/dist/core/RequirementService.js +0 -24
  6. package/dist/core/installers/clients/BaseClientInstaller.d.ts +1 -1
  7. package/dist/core/installers/clients/ClientInstaller.d.ts +1 -1
  8. package/dist/core/installers/clients/ClientInstaller.js +1 -1
  9. package/dist/core/installers/clients/ClientInstallerFactory.js +1 -1
  10. package/dist/core/installers/clients/ClineInstaller.d.ts +1 -1
  11. package/dist/core/installers/clients/ClineInstaller.js +1 -1
  12. package/dist/core/installers/clients/ExtensionInstaller.js +1 -1
  13. package/dist/core/installers/clients/GithubCopilotInstaller.d.ts +1 -1
  14. package/dist/core/installers/clients/GithubCopilotInstaller.js +1 -1
  15. package/dist/core/installers/clients/MSRooCodeInstaller.d.ts +1 -1
  16. package/dist/core/installers/clients/MSRooCodeInstaller.js +1 -1
  17. package/dist/core/installers/requirements/BaseInstaller.d.ts +1 -1
  18. package/dist/core/installers/requirements/BaseInstaller.js +1 -1
  19. package/dist/core/installers/requirements/CommandInstaller.d.ts +1 -1
  20. package/dist/core/installers/requirements/CommandInstaller.js +1 -1
  21. package/dist/core/installers/requirements/GeneralInstaller.d.ts +1 -1
  22. package/dist/core/installers/requirements/InstallerFactory.d.ts +1 -1
  23. package/dist/core/installers/requirements/NpmInstaller.d.ts +1 -1
  24. package/dist/core/installers/requirements/NpmInstaller.js +1 -1
  25. package/dist/core/installers/requirements/PipInstaller.d.ts +1 -1
  26. package/dist/core/installers/requirements/RequirementInstaller.d.ts +1 -1
  27. package/dist/core/loaders/ConfigurationLoader.d.ts +32 -0
  28. package/dist/core/loaders/ConfigurationLoader.js +236 -0
  29. package/dist/core/loaders/ConfigurationProvider.d.ts +35 -0
  30. package/dist/core/loaders/ConfigurationProvider.js +375 -0
  31. package/dist/core/loaders/ServerSchemaLoader.d.ts +11 -0
  32. package/{src/core/ServerSchemaLoader.ts → dist/core/loaders/ServerSchemaLoader.js} +43 -48
  33. package/dist/core/loaders/ServerSchemaProvider.d.ts +17 -0
  34. package/{src/core/ServerSchemaProvider.ts → dist/core/loaders/ServerSchemaProvider.js} +120 -137
  35. package/dist/core/metadatas/constants.d.ts +47 -0
  36. package/dist/core/metadatas/constants.js +94 -0
  37. package/dist/core/metadatas/types.d.ts +166 -0
  38. package/dist/core/metadatas/types.js +16 -0
  39. package/dist/core/onboard/FeedOnboardService.d.ts +1 -1
  40. package/dist/core/onboard/FeedOnboardService.js +1 -1
  41. package/dist/core/onboard/OnboardProcessor.d.ts +1 -1
  42. package/dist/core/onboard/OnboardProcessor.js +1 -1
  43. package/dist/core/onboard/OnboardStatus.d.ts +1 -1
  44. package/dist/core/onboard/OnboardStatusManager.d.ts +1 -1
  45. package/dist/core/onboard/OnboardStatusManager.js +1 -1
  46. package/dist/core/validators/FeedValidator.d.ts +1 -1
  47. package/dist/core/validators/IServerValidator.d.ts +1 -1
  48. package/dist/core/validators/SSEServerValidator.d.ts +1 -1
  49. package/dist/core/validators/ServerValidatorFactory.d.ts +1 -1
  50. package/dist/core/validators/StdioServerValidator.d.ts +1 -1
  51. package/dist/core/validators/StdioServerValidator.js +1 -1
  52. package/dist/index.d.ts +3 -3
  53. package/dist/index.js +3 -3
  54. package/dist/services/InstallationService.d.ts +50 -0
  55. package/dist/services/InstallationService.js +350 -0
  56. package/dist/services/MCPManager.d.ts +28 -0
  57. package/dist/services/MCPManager.js +188 -0
  58. package/dist/services/RequirementService.d.ts +40 -0
  59. package/dist/services/RequirementService.js +110 -0
  60. package/dist/services/ServerService.d.ts +2 -2
  61. package/dist/services/ServerService.js +5 -5
  62. package/dist/utils/adoUtils.d.ts +2 -2
  63. package/dist/utils/adoUtils.js +1 -1
  64. package/dist/utils/feedUtils.js +1 -1
  65. package/dist/utils/githubUtils.d.ts +1 -1
  66. package/dist/utils/githubUtils.js +1 -1
  67. package/dist/utils/logger.js +1 -1
  68. package/dist/utils/macroExpressionUtils.d.ts +1 -1
  69. package/dist/utils/osUtils.d.ts +1 -1
  70. package/dist/utils/osUtils.js +1 -1
  71. package/dist/web/contract/serverContract.d.ts +1 -1
  72. package/dist/web/public/index.html +1 -3
  73. package/dist/web/public/js/api.js +2 -80
  74. package/dist/web/server.js +2 -2
  75. package/package.json +1 -1
  76. package/src/cli/commands/install.ts +3 -3
  77. package/src/cli/commands/list.ts +2 -2
  78. package/src/cli/commands/serve.ts +3 -2
  79. package/src/cli/index.ts +1 -1
  80. package/src/core/installers/clients/BaseClientInstaller.ts +134 -3
  81. package/src/core/installers/clients/ClientInstaller.ts +3 -3
  82. package/src/core/installers/clients/ClientInstallerFactory.ts +1 -1
  83. package/src/core/installers/clients/ClineInstaller.ts +1 -101
  84. package/src/core/installers/clients/ExtensionInstaller.ts +1 -1
  85. package/src/core/installers/clients/GithubCopilotInstaller.ts +1 -101
  86. package/src/core/installers/clients/MSRooCodeInstaller.ts +1 -102
  87. package/src/core/installers/requirements/BaseInstaller.ts +2 -2
  88. package/src/core/installers/requirements/CommandInstaller.ts +1 -1
  89. package/src/core/installers/requirements/GeneralInstaller.ts +1 -1
  90. package/src/core/installers/requirements/InstallerFactory.ts +1 -1
  91. package/src/core/installers/requirements/NpmInstaller.ts +12 -12
  92. package/src/core/installers/requirements/PipInstaller.ts +1 -1
  93. package/src/core/installers/requirements/RequirementInstaller.ts +1 -1
  94. package/src/core/{ConfigurationLoader.ts → loaders/ConfigurationLoader.ts} +31 -7
  95. package/src/core/{ConfigurationProvider.ts → loaders/ConfigurationProvider.ts} +18 -10
  96. package/src/core/loaders/ServerSchemaLoader.ts +117 -0
  97. package/src/core/loaders/ServerSchemaProvider.ts +99 -0
  98. package/src/core/{types.ts → metadatas/types.ts} +3 -2
  99. package/src/core/onboard/FeedOnboardService.ts +270 -146
  100. package/src/core/onboard/OnboardProcessor.ts +60 -11
  101. package/src/core/onboard/OnboardStatus.ts +7 -2
  102. package/src/core/onboard/OnboardStatusManager.ts +270 -43
  103. package/src/core/validators/FeedValidator.ts +65 -9
  104. package/src/core/validators/IServerValidator.ts +1 -1
  105. package/src/core/validators/SSEServerValidator.ts +2 -2
  106. package/src/core/validators/ServerValidatorFactory.ts +1 -1
  107. package/src/core/validators/StdioServerValidator.ts +86 -34
  108. package/src/index.ts +3 -3
  109. package/src/{core → services}/InstallationService.ts +5 -5
  110. package/src/{core → services}/MCPManager.ts +10 -5
  111. package/src/{core → services}/RequirementService.ts +2 -31
  112. package/src/services/ServerService.ts +7 -7
  113. package/src/utils/adoUtils.ts +3 -3
  114. package/src/utils/feedUtils.ts +2 -2
  115. package/src/utils/githubUtils.ts +2 -2
  116. package/src/utils/logger.ts +13 -1
  117. package/src/utils/macroExpressionUtils.ts +1 -1
  118. package/src/utils/osUtils.ts +4 -4
  119. package/src/web/contract/serverContract.ts +2 -2
  120. package/src/web/public/index.html +1 -3
  121. package/src/web/public/js/api.js +2 -80
  122. package/src/web/public/js/modal/installation.js +1 -1
  123. package/src/web/public/js/onboard/ONBOARDING_PAGE_DESIGN.md +41 -9
  124. package/src/web/public/js/onboard/formProcessor.js +200 -34
  125. package/src/web/public/js/onboard/index.js +2 -2
  126. package/src/web/public/js/onboard/publishHandler.js +30 -22
  127. package/src/web/public/js/onboard/templates.js +34 -40
  128. package/src/web/public/js/onboard/uiHandlers.js +175 -84
  129. package/src/web/public/js/onboard/validationHandlers.js +147 -64
  130. package/src/web/public/js/serverCategoryDetails.js +19 -4
  131. package/src/web/public/js/serverCategoryList.js +13 -1
  132. package/src/web/public/onboard.html +1 -1
  133. package/src/web/server.ts +30 -14
  134. package/src/services/InstallRequestValidator.ts +0 -112
  135. /package/src/core/{constants.ts → metadatas/constants.ts} +0 -0
@@ -22,6 +22,11 @@ export async function handlePublish(event, activeTab, currentSelectedCategoryDat
22
22
  const { panelId, contentId, formId, validateButtonId, publishButtonId } = getElementIdsByTab(activeTab);
23
23
 
24
24
  const statusContentElement = document.getElementById(contentId);
25
+ // Ensure the progress toggle listener is attached when handlePublish is called
26
+ if (typeof ensureProgressToggleListener === 'function') { // Check if imported correctly
27
+ ensureProgressToggleListener(statusContentElement);
28
+ }
29
+
25
30
  const onboardForm = document.getElementById(formId);
26
31
  const validateButton = document.getElementById(validateButtonId);
27
32
  const publishButton = document.getElementById(publishButtonId);
@@ -48,21 +53,10 @@ export async function handlePublish(event, activeTab, currentSelectedCategoryDat
48
53
  if (forExistingCategoryTab && currentSelectedCategoryData) {
49
54
  finalFeedConfiguration = JSON.parse(JSON.stringify(currentSelectedCategoryData)); // Deep clone
50
55
 
51
- // Merge MCP Servers
52
- finalFeedConfiguration.mcpServers = (finalFeedConfiguration.mcpServers || []).concat(newServersData.mcpServers || []);
53
-
54
- // Merge Requirements (ensuring uniqueness)
55
- const existingReqKeys = new Set((finalFeedConfiguration.requirements || []).map(r => `${r.type}|${r.name}|${r.version}`));
56
- (newServersData.requirements || []).forEach(newReq => {
57
- const reqKey = `${newReq.type}|${newReq.name}|${newReq.version}`;
58
- if (!existingReqKeys.has(reqKey)) {
59
- if (!finalFeedConfiguration.requirements) {
60
- finalFeedConfiguration.requirements = [];
61
- }
62
- finalFeedConfiguration.requirements.push(newReq);
63
- existingReqKeys.add(reqKey);
64
- }
65
- });
56
+ // Replace MCP Servers and Requirements with the current state from the form (newServersData)
57
+ // This aligns with the logic in handleValidation
58
+ finalFeedConfiguration.mcpServers = newServersData.mcpServers || [];
59
+ finalFeedConfiguration.requirements = newServersData.requirements || [];
66
60
  } else {
67
61
  finalFeedConfiguration = newServersData;
68
62
  }
@@ -87,11 +81,18 @@ export async function handlePublish(event, activeTab, currentSelectedCategoryDat
87
81
 
88
82
  if (result.success && result.data) {
89
83
  updateOperationDisplay(result.data, statusContentElement);
90
- const categoryName = result.data.onboardingId || finalFeedConfiguration.name;
84
+ const categoryName = finalFeedConfiguration.name; // Use the original category name for polling
85
+ const operationTypeForPolling = 'FULL_ONBOARDING'; // Publish always initiates 'FULL_ONBOARDING'
91
86
  const initialStatus = result.data.status;
92
87
 
93
- if (categoryName && initialStatus !== 'COMPLETED' && initialStatus !== 'FAILED' && initialStatus !== 'succeeded') {
94
- pollingIntervalId = setInterval(() => pollOperationStatus(categoryName, contentId, validateButtonId, publishButtonId, 'publish'), POLLING_INTERVAL);
88
+ if (categoryName && typeof initialStatus === 'string' && initialStatus.toUpperCase() !== 'COMPLETED' && initialStatus.toUpperCase() !== 'FAILED' && initialStatus.toUpperCase() !== 'SUCCEEDED') {
89
+ pollingIntervalId = setInterval(async () => {
90
+ const shouldContinue = await pollOperationStatus(categoryName, contentId, validateButtonId, publishButtonId, 'publish', operationTypeForPolling);
91
+ if (!shouldContinue) {
92
+ clearInterval(pollingIntervalId);
93
+ pollingIntervalId = null;
94
+ }
95
+ }, POLLING_INTERVAL);
95
96
  } else {
96
97
  // Restore buttons to their original state fully
97
98
  publishButton.disabled = false;
@@ -100,15 +101,22 @@ export async function handlePublish(event, activeTab, currentSelectedCategoryDat
100
101
  validateButton.disabled = false;
101
102
  validateButton.innerHTML = "<i class='bx bx-check-shield mr-2'></i>Validate";
102
103
  validateButton.classList.remove('opacity-50');
103
- if (result.data.status === 'COMPLETED' || result.data.status === 'succeeded') {
104
+ if (initialStatus.toUpperCase() === 'COMPLETED' || initialStatus.toUpperCase() === 'SUCCEEDED') {
104
105
  showToast(result.data.message || 'Publish successful!', 'success');
105
- } else if (result.data.status === 'FAILED') {
106
+ } else if (initialStatus.toUpperCase() === 'FAILED') {
106
107
  showToast(result.data.errorMessage || result.data.message || 'Publish failed.', 'error');
107
108
  }
108
109
  }
109
110
  } else {
110
- statusContentElement.innerHTML = `<p class="text-red-500">Publish request failed: ${result.error || result.message || 'Unknown error'}</p>`;
111
- showToast(result.error || result.message || 'Publish request failed.', 'error');
111
+ // Handle cases where result.success is false or result.data is missing
112
+ const errorMessage = (result.data && result.data.message) || result.error || result.message || 'Publish request failed: Unknown error';
113
+ if (result.data) { // If data is present (even if success is false), update display
114
+ updateOperationDisplay(result.data, statusContentElement);
115
+ } else {
116
+ statusContentElement.innerHTML = `<p class="text-red-500">${errorMessage}</p>`;
117
+ }
118
+ showToast(errorMessage, 'error');
119
+
112
120
  // Restore buttons to their original state fully
113
121
  publishButton.disabled = false;
114
122
  publishButton.innerHTML = "<i class='bx bx-cloud-upload mr-2'></i>Publish";
@@ -10,31 +10,24 @@
10
10
  * @returns {string} HTML string for the server item.
11
11
  */
12
12
  export const serverTemplate = (serverIndex, isReadOnly = false, serverData = null, serversListId = 'serversList') => {
13
- const disabledAttr = isReadOnly ? 'disabled' : '';
14
- const readOnlyClasses = isReadOnly ? 'bg-gray-100 cursor-not-allowed opacity-70' : '';
15
- const hideButtonClass = isReadOnly ? 'hidden' : ''; // Class to hide buttons
13
+ const isServerAdhoc = serverData?.systemTags?.adhoc === 'true';
14
+ // Server is fully editable if the category isn't read-only OR if the server itself is adhoc.
15
+ const makeServerFullyEditable = !isReadOnly || isServerAdhoc;
16
+ // DEBUG: console.log(`[serverTemplate] ServerIndex: ${serverIndex}, isContextReadOnly(isReadOnly): ${isReadOnly}, isServerAdhoc: ${isServerAdhoc}, makeServerFullyEditable: ${makeServerFullyEditable}, flag for addReq/addEnv: ${!makeServerFullyEditable}`);
16
17
 
17
- // Determine which serversListId this server belongs to by checking the active tab's form
18
- // This is a bit of a hack; ideally, addServer in uiHandlers would pass this.
19
- // For now, assume 'serversList' or 'existingCategoryServersList' based on context where addServer is called.
20
- // The onclick handlers for removeServer, addServerRequirement, addEnvVariable will need to be updated
21
- // in uiHandlers.js to correctly pass the serversListId if they are to be generic.
22
- // For now, the template will use the serverIndex, and the global functions will need to know the context.
23
- // Let's assume the global functions are called with context from index.js or uiHandlers.js.
24
- // The onclicks in the template will call the global functions which are now context-aware.
25
- // e.g. onclick="removeServer(${serverIndex}, 'serversList')" - this needs to be dynamic.
26
- // For simplicity, let's assume the global functions in uiHandlers.js will determine the active list.
27
- // Or, the functions like removeServer are already modified to accept serversListId.
28
- // The `addServer` in `uiHandlers.js` is called with `serversListId`.
29
- // The buttons here call global functions. These global functions (removeServer, addEnvVariable, etc.)
30
- // will need to be aware of the active serversListId.
31
- // This can be done by having uiHandlers.js set a global 'activeServersListId' or by modifying
32
- // each function to accept it and ensuring the HTML onclicks pass it.
33
- // The latter is cleaner. The `onclick` in HTML should be `removeServer(${serverIndex}, 'CURRENT_LIST_ID')`.
34
- // This means `uiHandlers.addServer` needs to inject the correct `serversListId` into these onclicks.
18
+ const disabledAttr = makeServerFullyEditable ? '' : 'disabled';
19
+ const readOnlyClasses = makeServerFullyEditable ? '' : 'bg-gray-100 cursor-not-allowed opacity-70';
20
+
21
+ // "Remove Server" button should be hidden if the server is NOT fully editable.
22
+ // A server is fully editable if the context isn't read-only OR if it's an adhoc server.
23
+ const hideRemoveServerButtonClass = !makeServerFullyEditable ? 'hidden' : '';
24
+ // Other action buttons (add dependency, add env var) also respect makeServerFullyEditable.
25
+ const hideActionButtonsClass = !makeServerFullyEditable ? 'hidden' : '';
35
26
 
36
- // For now, let's keep the onclicks simple and assume uiHandlers.js functions will manage context.
37
- // The `action-button-in-server` class is added to buttons that should be hidden in read-only mode.
27
+
28
+ // Note: The 'serversListId' parameter is crucial for ensuring that actions (like removeServer, addEnvVariable)
29
+ // are correctly scoped to the server list in the current tab (e.g., 'serversList' or 'existingCategoryServersList').
30
+ // The onclick handlers in this template dynamically include this 'serversListId'.
38
31
 
39
32
  return `
40
33
  <div class="server-item p-4 border border-gray-300 rounded-lg mb-6 bg-white shadow" data-index="${serverIndex}">
@@ -43,13 +36,13 @@ export const serverTemplate = (serverIndex, isReadOnly = false, serverData = nul
43
36
  aria-expanded="false" aria-controls="${serversListId}-server-content-${serverIndex}"
44
37
  onclick="window.toggleSectionContent('${serversListId}-server-content-${serverIndex}', this.querySelector('i.toggle-icon'), this)"
45
38
  onkeydown="if(event.key==='Enter' || event.key===' ') { window.toggleSectionContent('${serversListId}-server-content-${serverIndex}', this.querySelector('i.toggle-icon'), this); event.preventDefault(); }">
46
- <h3 id="${serversListId}-server-title-${serverIndex}" class="text-lg font-semibold text-gray-800">MCP Server #${serverIndex + 1} ${isReadOnly ? '(Read-only)' : ''}</h3>
47
- <div class="flex items-center">
48
- <button type="button" onclick="event.stopPropagation(); removeServer(${serverIndex}, '${serversListId}')"
49
- 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 ${hideButtonClass}" title="Remove Server">
50
- <i class='bx bx-trash text-lg'></i>
51
- </button>
52
- <i class='bx bxs-chevron-down text-xl toggle-icon'></i>
39
+ <h3 id="${serversListId}-server-title-${serverIndex}" class="text-lg font-semibold text-gray-800">MCP Server #${serverIndex + 1}${isServerAdhoc ? ' <span class="text-sm text-blue-600 ml-1">(Adhoc - Editable)</span>' : ''}</h3>
40
+ <div class="flex items-center">
41
+ <button type="button" onclick="event.stopPropagation(); removeServer(${serverIndex}, '${serversListId}')"
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
+ <i class='bx bx-trash text-lg'></i>
44
+ </button>
45
+ <i class='bx bxs-chevron-down text-xl toggle-icon'></i>
53
46
  </div>
54
47
  </div>
55
48
  <div id="${serversListId}-server-content-${serverIndex}" class="collapsible-content server-content-scrollable hidden" role="region" aria-labelledby="${serversListId}-server-title-${serverIndex}">
@@ -77,15 +70,15 @@ export const serverTemplate = (serverIndex, isReadOnly = false, serverData = nul
77
70
  </div>
78
71
  <div class="md:col-span-2 flex gap-x-4 items-end">
79
72
  <div class="flex-grow">
80
- <label class="block text-sm font-medium text-gray-700 mb-1">Schema File Path</label>
73
+ <label class="block text-sm font-medium text-gray-700 mb-1">Schema File Path
74
+ <span class="text-xs text-gray-500 ml-1" title="Optional for listing available tools, please fill full path of the schema file">
75
+ <i class='bx bx-info-circle'></i> Optional
76
+ </span>
77
+ </label>
81
78
  <input type="text" name="servers[${serverIndex}].schemas" id="schema-path-${serverIndex}" ${disabledAttr}
82
79
  class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${readOnlyClasses}"
83
- placeholder="e.g., ./schemas/my_server_schema.json">
80
+ placeholder="e.g., /Users/xxx/schemas/my_server_schema.json">
84
81
  </div>
85
- <button type="button" onclick="browseLocalSchema(${serverIndex}, '${serversListId}')" ${disabledAttr}
86
- class="action-button-in-server px-4 py-2 text-sm bg-gray-100 hover:bg-gray-200 border border-gray-300 rounded-lg whitespace-nowrap ${hideButtonClass} ${isReadOnly ? 'opacity-50 cursor-not-allowed' : ''}">
87
- Browse Local
88
- </button>
89
82
  </div>
90
83
  <div class="md:col-span-2">
91
84
  <label class="block text-sm font-medium text-gray-700 mb-1">Repository URL</label>
@@ -109,8 +102,8 @@ export const serverTemplate = (serverIndex, isReadOnly = false, serverData = nul
109
102
  <div id="server-requirements-list-${serverIndex}" class="space-y-4">
110
103
  <!-- Server requirements will be populated here by serverRequirementTemplate -->
111
104
  </div>
112
- <button type="button" onclick="addServerRequirement(${serverIndex}, '${serversListId}', ${isReadOnly})"
113
- class="action-button-in-server mt-3 px-3 py-1.5 text-sm border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-100 flex items-center ${hideButtonClass}">
105
+ <button type="button" onclick="addServerRequirement(${serverIndex}, '${serversListId}', ${!makeServerFullyEditable})"
106
+ class="action-button-in-server mt-3 px-3 py-1.5 text-sm border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-100 flex items-center ${hideActionButtonsClass}">
114
107
  <i class='bx bx-plus mr-1'></i> Add Dependency
115
108
  </button>
116
109
  </div>
@@ -147,8 +140,8 @@ export const serverTemplate = (serverIndex, isReadOnly = false, serverData = nul
147
140
  <div id="envVarsContainer_${serverIndex}" class="space-y-3">
148
141
  <!-- Environment variables will be added here by envVariableTemplate -->
149
142
  </div>
150
- <button type="button" onclick="addEnvVariable(${serverIndex}, '${serversListId}', ${isReadOnly})"
151
- class="action-button-in-server mt-3 px-3 py-1.5 text-sm border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-100 flex items-center ${hideButtonClass}">
143
+ <button type="button" onclick="addEnvVariable(${serverIndex}, '${serversListId}', ${!makeServerFullyEditable})"
144
+ class="action-button-in-server mt-3 px-3 py-1.5 text-sm border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-100 flex items-center ${hideActionButtonsClass}">
152
145
  <i class='bx bx-plus mr-1'></i> Add Environment Variable
153
146
  </button>
154
147
  </div>
@@ -217,6 +210,7 @@ export const envVariableTemplate = (serverIndex, envIndex, isReadOnly = false, s
217
210
  * @returns {string} HTML string for the server requirement item.
218
211
  */
219
212
  export const serverRequirementTemplate = (serverIndex, reqIndex, isReadOnly = false, serversListId = 'serversList') => {
213
+ // DEBUG: console.log(`[serverRequirementTemplate] ServerIndex: ${serverIndex}, ReqIndex: ${reqIndex}, isReadOnly received: ${isReadOnly}`);
220
214
  const disabledAttr = isReadOnly ? 'disabled' : '';
221
215
  const readOnlyClasses = isReadOnly ? 'bg-gray-100 cursor-not-allowed opacity-70' : '';
222
216
  const hideButtonClass = isReadOnly ? 'hidden' : '';
@@ -54,19 +54,61 @@ function reindexServers(serversListId = 'serversList') {
54
54
 
55
55
  const titleElement = serverItem.querySelector('h3');
56
56
  if (titleElement) {
57
+ const originalTextContent = titleElement.textContent || '';
57
58
  const baseTitle = `MCP Server #${newServerIndex + 1}`;
58
- const readOnlySuffix = titleElement.textContent.includes('(Read-only)') ? ' (Read-only)' : '';
59
- titleElement.textContent = `${baseTitle}${readOnlySuffix}`;
59
+ let suffix = '';
60
+ if (originalTextContent.includes('(Adhoc - Editable)')) {
61
+ suffix = ' <span class="text-sm text-blue-600 ml-1">(Adhoc - Editable)</span>';
62
+ } else if (originalTextContent.includes('(Read-only)')) {
63
+ suffix = ' (Read-only)';
64
+ }
65
+ // Use innerHTML to preserve the span if it's an adhoc server
66
+ titleElement.innerHTML = `${baseTitle}${suffix}`;
60
67
  }
61
68
 
62
69
  // Update onclick handlers
70
+ // Ensure the server header toggle onclick is correctly re-indexed for its ID parameters
71
+ const headerToggle = serverItem.querySelector(`#${serversListId}-server-header-${oldServerIndex}`);
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);
87
+ }
88
+ // Update title ID for aria-labelledby
89
+ const titleForAria = serverItem.querySelector(`#${serversListId}-server-title-${oldServerIndex}`);
90
+ if (titleForAria) {
91
+ titleForAria.id = `${serversListId}-server-title-${newServerIndex}`;
92
+ }
93
+ const contentRegion = serverItem.querySelector(`#${serversListId}-server-content-${oldServerIndex}`);
94
+ if (contentRegion) {
95
+ contentRegion.setAttribute('aria-labelledby', `${serversListId}-server-title-${newServerIndex}`);
96
+ }
97
+
98
+
63
99
  serverItem.querySelectorAll('[onclick]').forEach(element => {
64
100
  const onclickAttr = element.getAttribute('onclick');
65
101
  if (!onclickAttr) return;
66
102
 
103
+ // Skip the header toggle as it's handled above to be more precise with ID replacement
104
+ if (element.id === `${serversListId}-server-header-${newServerIndex}`) return;
105
+
67
106
  const updatedOnclick = onclickAttr
68
107
  .replace(new RegExp(`\\((\\s*)${oldServerIndex}(\\s*[,\\)])`, 'g'), `($1${newServerIndex}$2`)
69
- .replace(new RegExp(`(${serversListId}-(?:server|installation|env-vars)-content-${oldServerIndex})`, 'g'), `$1${newServerIndex}`);
108
+ // More specific replacement for content IDs to avoid accidental replacements
109
+ .replace(new RegExp(`${serversListId}-server-deps-content-${oldServerIndex}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), `${serversListId}-server-deps-content-${newServerIndex}`)
110
+ .replace(new RegExp(`${serversListId}-installation-content-${oldServerIndex}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), `${serversListId}-installation-content-${newServerIndex}`)
111
+ .replace(new RegExp(`${serversListId}-env-vars-content-${oldServerIndex}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), `${serversListId}-env-vars-content-${newServerIndex}`);
70
112
 
71
113
  element.setAttribute('onclick', updatedOnclick);
72
114
  });
@@ -137,13 +179,18 @@ export function addServer(serversListId = 'serversList', isContextGenerallyReadO
137
179
 
138
180
  const newServerIndex = getServerCounter(serversListId);
139
181
  let actualReadOnlyForThisServer;
182
+ const isServerAdhoc = serverData?.systemTags?.adhoc === 'true';
140
183
 
141
- if (!isContextGenerallyReadOnly) {
184
+ if (isServerAdhoc) {
185
+ // Adhoc servers are always editable, regardless of the general context.
186
+ actualReadOnlyForThisServer = false;
187
+ } else if (!isContextGenerallyReadOnly) {
142
188
  // Context is not generally read-only (e.g., creating a brand new category,
143
189
  // or user clicked "Add Server" button when no specific read-only category context applies).
144
190
  actualReadOnlyForThisServer = false;
145
191
  } else {
146
- // Context IS generally read-only (e.g., an existing category was selected, or we are populating from JSON for such a category).
192
+ // Context IS generally read-only AND the server is NOT adhoc.
193
+ // Determine read-only status based on whether it's an original server or a newly added one.
147
194
  if (state.originalServerNamesForFormPopulation) {
148
195
  // We are in the process of toggling from JSON view back to Form view for an existing category.
149
196
  // state.originalServerNamesForFormPopulation contains names of servers that were part of the category *before* any UI/JSON edits.
@@ -152,7 +199,6 @@ export function addServer(serversListId = 'serversList', isContextGenerallyReadO
152
199
  actualReadOnlyForThisServer = true;
153
200
  } else {
154
201
  // This serverData (from JSON) is new (wasn't in originalServerNamesForFormPopulation). It should be editable.
155
- // This also covers cases where serverData might not have a name yet if added directly in JSON.
156
202
  actualReadOnlyForThisServer = false;
157
203
  }
158
204
  } else {
@@ -160,7 +206,7 @@ export function addServer(serversListId = 'serversList', isContextGenerallyReadO
160
206
  // 1. Initial population of an existing category's servers (serverData will be provided).
161
207
  // 2. User clicked the "Add Server" button while an existing category context is active (serverData will be null).
162
208
  if (serverData) {
163
- // Case 1: Initial population of an existing server from category data. Should be read-only.
209
+ // Case 1: Initial population of an existing server from category data. Should be read-only (unless it was adhoc, handled above).
164
210
  actualReadOnlyForThisServer = true;
165
211
  } else {
166
212
  // Case 2: User clicked "Add Server" button. The new server item should be editable.
@@ -168,8 +214,10 @@ export function addServer(serversListId = 'serversList', isContextGenerallyReadO
168
214
  }
169
215
  }
170
216
  }
171
-
172
- container.insertAdjacentHTML('beforeend', serverTemplate(newServerIndex, actualReadOnlyForThisServer, serverData, serversListId));
217
+ // `isContextGenerallyReadOnly`: Influences initial template state for some UI elements (e.g., button visibility).
218
+ // `actualReadOnlyForThisServer`: Determines if fields should be disabled and is passed to setupReadOnlyState.
219
+ // `serverData`: Used by the template for initial adhoc title span, and passed to setupReadOnlyState for more checks.
220
+ container.insertAdjacentHTML('beforeend', serverTemplate(newServerIndex, isContextGenerallyReadOnly, serverData, serversListId));
173
221
 
174
222
  setEnvCounter(serversListId, newServerIndex, 0);
175
223
  setServerRequirementCounter(serversListId, newServerIndex, 0);
@@ -177,8 +225,20 @@ export function addServer(serversListId = 'serversList', isContextGenerallyReadO
177
225
 
178
226
  const serverItem = container.querySelector(`.server-item[data-index="${newServerIndex}"]`);
179
227
  if (serverItem) {
228
+ if (serverData && serverData.systemTags) { // Case 1: Populating from existing serverData that has systemTags
229
+ 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.
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.
237
+ delete serverItem.dataset.systemTags;
238
+ }
180
239
  setupServerMode(serverItem, newServerIndex, serversListId, actualReadOnlyForThisServer, serverData);
181
- setupReadOnlyState(serverItem, actualReadOnlyForThisServer, serversListId, newServerIndex);
240
+ // Pass serverData to setupReadOnlyState so it can accurately determine adhoc status for title.
241
+ setupReadOnlyState(serverItem, actualReadOnlyForThisServer, serverData, serversListId, newServerIndex);
182
242
 
183
243
  // Default expand Package Dependencies, Startup Configuration, and Environment Variables sections
184
244
  const sections = [
@@ -247,15 +307,56 @@ function setupServerMode(serverItem, serverIndex, serversListId, isReadOnly, ser
247
307
  renderInstallationConfig(serverIndex, serversListId, modeSelect.value, isReadOnly, serverData?.installation);
248
308
  }
249
309
 
250
- function setupReadOnlyState(serverItem, isReadOnly, serversListId, serverIndex) {
251
- if (isReadOnly) {
310
+ // `isEffectivelyReadOnly` determines if the fields within this server item should be disabled.
311
+ // `serverDataFromPopulation` is the original server data object passed during population, used to check initial adhoc status.
312
+ function setupReadOnlyState(serverItem, isEffectivelyReadOnly, serverDataFromPopulation, serversListId, serverIndex) {
313
+ const titleElement = serverItem.querySelector(`#${serversListId}-server-title-${serverIndex}`);
314
+ const baseTitle = `MCP Server #${serverIndex + 1}`;
315
+ let isAdhocForTitle = serverDataFromPopulation?.systemTags?.adhoc === 'true';
316
+
317
+ // Double-check adhoc status from dataset if serverDataFromPopulation doesn't indicate it
318
+ // (e.g. if it became adhoc after JSON edit and re-population)
319
+ if (!isAdhocForTitle && serverItem.dataset.systemTags) {
320
+ try {
321
+ const tags = JSON.parse(serverItem.dataset.systemTags);
322
+ if (tags.adhoc === "true") {
323
+ isAdhocForTitle = true;
324
+ }
325
+ } catch (e) { /* ignore parsing error for title determination */ }
326
+ }
327
+
328
+ if (isEffectivelyReadOnly) {
329
+ if (titleElement) {
330
+ if (!isAdhocForTitle) {
331
+ titleElement.innerHTML = `${baseTitle} (Read-only)`;
332
+ } else {
333
+ // For adhoc servers, the template is responsible for the "(Adhoc - Editable)" span.
334
+ // If it's somehow missing and it's adhoc, ensure it's present.
335
+ // This situation should be rare if template logic is correct.
336
+ if (!titleElement.querySelector('span.text-blue-600')) {
337
+ titleElement.innerHTML = `${baseTitle} <span class="text-sm text-blue-600 ml-1">(Adhoc - Editable)</span>`;
338
+ } 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}`;
342
+ }
343
+ }
344
+ }
345
+ // Disable all input fields within this specific server item
252
346
  serverItem.querySelectorAll('input, select, textarea').forEach(el => {
253
- el.disabled = true;
254
- el.classList.add('bg-gray-100', 'cursor-not-allowed', 'opacity-70');
347
+ // Check if the element is part of a sub-item (like env var or requirement)
348
+ // These sub-items have their own read-only logic handled by their add functions.
349
+ // We only want to disable the main server fields here.
350
+ if (!el.closest('.env-var-item') && !el.closest('.server-requirement-item')) {
351
+ el.disabled = true;
352
+ el.classList.add('bg-gray-100', 'cursor-not-allowed', 'opacity-70');
353
+ }
255
354
  });
256
355
 
356
+ // Hide all action buttons (Add Dependency, Add Env Var, Remove Server) for this server if it's read-only.
257
357
  serverItem.querySelectorAll('.action-button-in-server').forEach(btn => {
258
- btn.style.display = 'none';
358
+ btn.style.display = 'none';
359
+ btn.classList.add('hidden');
259
360
  });
260
361
 
261
362
  // Expand server content and all key sections
@@ -275,18 +376,42 @@ function setupReadOnlyState(serverItem, isReadOnly, serversListId, serverIndex)
275
376
  iconElement.classList.add('bxs-chevron-up');
276
377
  }
277
378
  });
278
- } else {
379
+ } else { // Server is effectively editable
380
+ if (titleElement) {
381
+ // Server is editable. Template handles the adhoc span.
382
+ // We just ensure the base title is correct.
383
+ // If adhoc, the template adds the span. If not adhoc, it's just the base title.
384
+ if (isAdhocForTitle) {
385
+ // Check if the adhoc span is already there from the template.
386
+ // If not (e.g. server became adhoc after initial render and this is a re-evaluation), add it.
387
+ if (!titleElement.querySelector('span.text-blue-600')) {
388
+ titleElement.innerHTML = `${baseTitle} <span class="text-sm text-blue-600 ml-1">(Adhoc - Editable)</span>`;
389
+ } 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}`;
393
+ }
394
+ } else {
395
+ // Editable and not adhoc, just the base title.
396
+ titleElement.innerHTML = baseTitle;
397
+ }
398
+ }
279
399
  setTimeout(() => {
400
+ // Enable main server fields
280
401
  serverItem.querySelectorAll('input, select, textarea').forEach(el => {
281
- el.disabled = false;
282
- el.removeAttribute('readonly');
283
- el.classList.remove('bg-gray-100', 'cursor-not-allowed', 'opacity-70');
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');
406
+ }
284
407
  });
285
408
 
409
+ // Show ALL action buttons for this server (Add Dependency, Add Env Var, Remove Server)
410
+ // as the server is determined to be effectively editable.
286
411
  serverItem.querySelectorAll('.action-button-in-server').forEach(btn => {
287
- btn.style.display = '';
288
- btn.disabled = false;
289
- btn.classList.remove('hidden', 'opacity-50', 'cursor-not-allowed');
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');
290
415
  });
291
416
  }, 0);
292
417
  }
@@ -361,24 +486,19 @@ export function removeServer(serverIndexToRemove, serversListId = 'serversList')
361
486
  }
362
487
  }
363
488
 
364
- export function addEnvVariable(serverIndex, serversListId = 'serversList', isReadOnly = false) {
489
+ // `isServerEffectivelyReadOnly` is passed to determine if the new env var item itself should be read-only.
490
+ // This would be true if the parent server is non-adhoc and in a read-only category.
491
+ export function addEnvVariable(serverIndex, serversListId = 'serversList', isServerEffectivelyReadOnly = false) {
365
492
  const container = document.querySelector(`#${serversListId} .server-item[data-index="${serverIndex}"] #envVarsContainer_${serverIndex}`);
366
493
  if (!container) return -1;
367
494
 
368
495
  const envIndex = getEnvCounter(serversListId, serverIndex);
369
- container.insertAdjacentHTML('beforeend', envVariableTemplate(serverIndex, envIndex, isReadOnly, serversListId));
496
+ // Pass `isServerEffectivelyReadOnly` to the template for the new env var item.
497
+ container.insertAdjacentHTML('beforeend', envVariableTemplate(serverIndex, envIndex, isServerEffectivelyReadOnly, serversListId));
370
498
  setEnvCounter(serversListId, serverIndex, envIndex + 1);
371
499
 
372
- if (isReadOnly) {
373
- const envItem = container.querySelector(`.env-var-item[data-env-index="${envIndex}"]`);
374
- if (envItem) {
375
- envItem.querySelectorAll('input, select, textarea').forEach(el => {
376
- el.disabled = true;
377
- el.classList.add('bg-gray-100', 'cursor-not-allowed', 'opacity-70');
378
- });
379
- envItem.querySelectorAll('.action-button-in-env').forEach(btn => btn.style.display = 'none');
380
- }
381
- }
500
+ // The template itself handles disabling fields if isServerEffectivelyReadOnly is true.
501
+ // No need for additional logic here to disable fields of the newly added item.
382
502
 
383
503
  return envIndex;
384
504
  }
@@ -388,24 +508,18 @@ export function removeEnvVariable(serverIndex, envIndex, serversListId = 'server
388
508
  if (item) item.remove();
389
509
  }
390
510
 
391
- export function addServerRequirement(serverIndex, serversListId = 'serversList', isReadOnly = false) {
511
+ // `isServerEffectivelyReadOnly` is passed to determine if the new requirement item itself should be read-only.
512
+ export function addServerRequirement(serverIndex, serversListId = 'serversList', isServerEffectivelyReadOnly = false) {
513
+ console.log(`[addServerRequirement] ServerIndex: ${serverIndex}, isServerEffectivelyReadOnly received: ${isServerEffectivelyReadOnly}`); // DEBUG
392
514
  const container = document.querySelector(`#${serversListId} .server-item[data-index="${serverIndex}"] #server-requirements-list-${serverIndex}`);
393
515
  if (!container) return -1;
394
516
 
395
517
  const reqIndex = getServerRequirementCounter(serversListId, serverIndex);
396
- container.insertAdjacentHTML('beforeend', serverRequirementTemplate(serverIndex, reqIndex, isReadOnly, serversListId));
518
+ // Pass `isServerEffectivelyReadOnly` to the template for the new requirement item.
519
+ container.insertAdjacentHTML('beforeend', serverRequirementTemplate(serverIndex, reqIndex, isServerEffectivelyReadOnly, serversListId));
397
520
  setServerRequirementCounter(serversListId, serverIndex, reqIndex + 1);
398
521
 
399
- if (isReadOnly) {
400
- const reqItem = container.querySelector(`.server-requirement-item[data-req-index="${reqIndex}"]`);
401
- if (reqItem) {
402
- reqItem.querySelectorAll('input, select, textarea').forEach(el => {
403
- el.disabled = true;
404
- el.classList.add('bg-gray-100', 'cursor-not-allowed', 'opacity-70');
405
- });
406
- reqItem.querySelectorAll('.action-button-in-req').forEach(btn => btn.style.display = 'none');
407
- }
408
- }
522
+ // The template itself handles disabling fields if isServerEffectivelyReadOnly is true.
409
523
 
410
524
  toggleServerAliasField(serverIndex, reqIndex, serversListId);
411
525
  toggleServerRegistryConfig(serverIndex, reqIndex, serversListId);
@@ -444,33 +558,6 @@ export function toggleServerRegistryConfig(serverIndex, reqIndex, serversListId
444
558
  });
445
559
  }
446
560
 
447
- export async function browseLocalSchema(serverIndex, serversListId = 'serversList') {
448
- const schemaPath = document.querySelector(`#${serversListId} .server-item[data-index="${serverIndex}"] #schema-path-${serverIndex}`);
449
- if (!schemaPath) return;
450
-
451
- try {
452
- if ('showOpenFilePicker' in window) {
453
- const [fileHandle] = await window.showOpenFilePicker({
454
- types: [{ description: 'JSON Files', accept: { 'application/json': ['.json'] } }]
455
- });
456
- const file = await fileHandle.getFile();
457
- schemaPath.value = file.name;
458
- } else {
459
- const input = document.createElement('input');
460
- input.type = 'file';
461
- input.accept = '.json';
462
- input.onchange = (e) => {
463
- if (e.target.files.length > 0) {
464
- schemaPath.value = e.target.files[0].name;
465
- }
466
- };
467
- input.click();
468
- }
469
- } catch (err) {
470
- console.error('Error browsing for schema file:', err);
471
- }
472
- }
473
-
474
561
  export function toggleSectionContent(contentId, iconElement, toggleElement = null) {
475
562
  const contentElement = document.getElementById(contentId);
476
563
  if (!contentElement) return;
@@ -588,23 +675,28 @@ function handleFormView(elements, currentFormId, currentServersListId, baseCateg
588
675
 
589
676
  function getFeedConfiguration(activeForm, baseCategoryData, isExistingCategoryContext) {
590
677
  if (!isExistingCategoryContext || !baseCategoryData) {
678
+ // If not in an existing category context, or no base data, just get current form data.
591
679
  return activeForm ? getFormData(activeForm) : {};
592
680
  }
593
681
 
682
+ // In an existing category context, get the data from the current form.
683
+ // `true` for forExistingCategoryTab ensures getFormData processes only server-related fields
684
+ // and correctly handles adhoc/new servers.
594
685
  const newData = getFormData(activeForm, true, baseCategoryData);
686
+
687
+ // Start with a deep clone of the original category data (for name, description, etc.)
595
688
  const merged = JSON.parse(JSON.stringify(baseCategoryData));
596
689
 
597
- merged.mcpServers = (merged.mcpServers || []).concat(newData.mcpServers || []);
690
+ // Replace mcpServers with the current state from the form (newData).
691
+ // This ensures deletions and modifications in the form are accurately reflected.
692
+ // newData.mcpServers will include original read-only servers (if any),
693
+ // modified adhoc servers, and newly added servers, all with their current state.
694
+ merged.mcpServers = newData.mcpServers || [];
598
695
 
599
- const existingReqs = new Set(merged.requirements?.map(r => `${r.type}|${r.name}|${r.version}`) || []);
600
- (newData.requirements || []).forEach(req => {
601
- const key = `${req.type}|${req.name}|${req.version}`;
602
- if (!existingReqs.has(key)) {
603
- merged.requirements = merged.requirements || [];
604
- merged.requirements.push(req);
605
- existingReqs.add(key);
606
- }
607
- });
696
+ // Requirements should also be derived from the current state of servers in the form.
697
+ // formDataToFeedConfiguration (which produces newData) calculates global requirements
698
+ // based on the servers it finds. So, newData.requirements should be the correct set.
699
+ merged.requirements = newData.requirements || [];
608
700
 
609
701
  return merged;
610
702
  }
@@ -749,7 +841,6 @@ Object.entries({
749
841
  removeServerRequirement,
750
842
  toggleServerAliasField,
751
843
  toggleServerRegistryConfig,
752
- browseLocalSchema,
753
844
  renderInstallationConfig,
754
845
  toggleSectionContent,
755
846
  copyJsonToClipboard