imcp 0.0.17 → 0.0.18

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 (46) hide show
  1. package/dist/cli/commands/serve.js +2 -1
  2. package/dist/core/installers/clients/BaseClientInstaller.d.ts +25 -2
  3. package/dist/core/installers/clients/BaseClientInstaller.js +121 -0
  4. package/dist/core/installers/clients/ClineInstaller.d.ts +1 -6
  5. package/dist/core/installers/clients/ClineInstaller.js +1 -94
  6. package/dist/core/installers/clients/GithubCopilotInstaller.d.ts +1 -6
  7. package/dist/core/installers/clients/GithubCopilotInstaller.js +1 -94
  8. package/dist/core/installers/clients/MSRooCodeInstaller.d.ts +1 -5
  9. package/dist/core/installers/clients/MSRooCodeInstaller.js +1 -94
  10. package/dist/core/loaders/ConfigurationLoader.d.ts +4 -1
  11. package/dist/core/loaders/ConfigurationLoader.js +24 -3
  12. package/dist/core/loaders/ConfigurationProvider.d.ts +4 -1
  13. package/dist/core/loaders/ConfigurationProvider.js +13 -4
  14. package/dist/core/loaders/ServerSchemaLoader.d.ts +15 -4
  15. package/dist/core/loaders/ServerSchemaLoader.js +86 -20
  16. package/dist/core/loaders/ServerSchemaProvider.d.ts +2 -5
  17. package/dist/core/loaders/ServerSchemaProvider.js +32 -62
  18. package/dist/core/metadatas/types.d.ts +3 -2
  19. package/dist/core/onboard/FeedOnboardService.d.ts +14 -7
  20. package/dist/core/onboard/FeedOnboardService.js +214 -129
  21. package/dist/core/onboard/OnboardProcessor.d.ts +7 -1
  22. package/dist/core/onboard/OnboardProcessor.js +52 -8
  23. package/dist/core/onboard/OnboardStatus.d.ts +6 -1
  24. package/dist/core/onboard/OnboardStatusManager.d.ts +70 -24
  25. package/dist/core/onboard/OnboardStatusManager.js +230 -46
  26. package/dist/core/validators/FeedValidator.d.ts +7 -2
  27. package/dist/core/validators/FeedValidator.js +61 -7
  28. package/dist/core/validators/StdioServerValidator.js +84 -32
  29. package/dist/services/MCPManager.d.ts +2 -1
  30. package/dist/services/MCPManager.js +5 -1
  31. package/dist/services/ServerService.js +2 -2
  32. package/dist/utils/logger.d.ts +2 -0
  33. package/dist/utils/logger.js +10 -0
  34. package/dist/web/public/js/modal/installation.js +1 -1
  35. package/dist/web/public/js/onboard/ONBOARDING_PAGE_DESIGN.md +41 -9
  36. package/dist/web/public/js/onboard/formProcessor.js +200 -34
  37. package/dist/web/public/js/onboard/index.js +2 -2
  38. package/dist/web/public/js/onboard/publishHandler.js +30 -22
  39. package/dist/web/public/js/onboard/templates.js +34 -40
  40. package/dist/web/public/js/onboard/uiHandlers.js +175 -84
  41. package/dist/web/public/js/onboard/validationHandlers.js +147 -64
  42. package/dist/web/public/js/serverCategoryDetails.js +19 -4
  43. package/dist/web/public/js/serverCategoryList.js +13 -1
  44. package/dist/web/public/onboard.html +1 -1
  45. package/dist/web/server.js +19 -6
  46. package/package.json +1 -1
@@ -172,14 +172,26 @@ export async function submitForm(event, activeTab, currentSelectedCategoryData =
172
172
  showToast('No existing category selected or category data is missing.', 'error');
173
173
  return;
174
174
  }
175
- const newServersData = formDataToFeedConfiguration(formElement, true, currentSelectedCategoryData);
176
175
 
177
- feedConfiguration = JSON.parse(JSON.stringify(currentSelectedCategoryData)); // Deep clone
176
+ // Get the complete state of servers and their requirements from the current form.
177
+ // The `true` flag for `forExistingCategoryTab` tells formDataToFeedConfiguration
178
+ // to only process server data and not category-level fields from this form.
179
+ // It should return ALL servers currently in this form, including modified adhoc and new ones.
180
+ const formDerivedData = formDataToFeedConfiguration(formElement, true, currentSelectedCategoryData);
178
181
 
179
- feedConfiguration.mcpServers = (feedConfiguration.mcpServers || []).concat(newServersData.mcpServers || []);
182
+ // Start with a deep clone of the original category data (for name, description, etc.)
183
+ feedConfiguration = JSON.parse(JSON.stringify(currentSelectedCategoryData));
180
184
 
185
+ // Replace the mcpServers list entirely with what was parsed from the form.
186
+ // This ensures that the list reflects the current state of the form (original read-only, modified adhoc, new).
187
+ feedConfiguration.mcpServers = formDerivedData.mcpServers || [];
188
+
189
+ // Merge requirements: start with original requirements, then add any new ones from the form.
190
+ // `formDerivedData.requirements` should ideally only contain requirements introduced by
191
+ // servers in the current form that are not already in `currentSelectedCategoryData.requirements`.
192
+ // The existing logic for merging requirements seems okay if `formDerivedData.requirements` is correctly populated.
181
193
  const existingReqKeys = new Set((feedConfiguration.requirements || []).map(r => `${r.type}|${r.name}|${r.version}`));
182
- (newServersData.requirements || []).forEach(newReq => {
194
+ (formDerivedData.requirements || []).forEach(newReq => {
183
195
  const reqKey = `${newReq.type}|${newReq.name}|${newReq.version}`;
184
196
  if (!existingReqKeys.has(reqKey)) {
185
197
  feedConfiguration.requirements.push(newReq);
@@ -193,7 +205,7 @@ export async function submitForm(event, activeTab, currentSelectedCategoryData =
193
205
  }
194
206
 
195
207
  try {
196
- const response = await fetch('/api/categories/onboard', {
208
+ const response = await fetch('/api/categories/onboard', {
197
209
  method: 'POST',
198
210
  headers: { 'Content-Type': 'application/json' },
199
211
  body: JSON.stringify({
@@ -208,7 +220,7 @@ export async function submitForm(event, activeTab, currentSelectedCategoryData =
208
220
  }
209
221
 
210
222
  const result = await response.json();
211
- console.log('Form submitted successfully:', result);
223
+ // console.log('Form submitted successfully:', result);
212
224
  showToast('Form submitted successfully! See console for operation details.', 'success');
213
225
  } catch (error) {
214
226
  console.error('Error submitting form:', error);
@@ -327,6 +339,15 @@ export function formDataToFeedConfiguration(formElement, forExistingCategoryTab
327
339
  mcpServers: []
328
340
  };
329
341
 
342
+ let serversListId;
343
+ if (formElement.id === 'onboardForm') {
344
+ serversListId = 'serversList';
345
+ } else if (formElement.id === 'onboardServerForm') {
346
+ serversListId = 'existingCategoryServersList';
347
+ } else {
348
+ console.warn('[formDataToFeedConfiguration] Could not determine serversListId from formElement.id:', formElement.id);
349
+ }
350
+
330
351
  if (!forExistingCategoryTab) {
331
352
  feedConfiguration.name = currentFormData.get('name') || '';
332
353
  feedConfiguration.displayName = currentFormData.get('displayName') || '';
@@ -420,8 +441,13 @@ export function formDataToFeedConfiguration(formElement, forExistingCategoryTab
420
441
  }
421
442
  }
422
443
 
423
- serverDataMap.forEach(serverRaw => {
444
+ // console.log('[formDataToFeedConfiguration] serverDataMap keys before processing:', Array.from(serverDataMap.keys())); // DEBUG
445
+ // console.log('[formDataToFeedConfiguration] serverDataMap size:', serverDataMap.size); // DEBUG
446
+
447
+ // Process servers found in the form data (new/adhoc or edited original servers)
448
+ serverDataMap.forEach((serverRaw, serverIndex) => {
424
449
  let mcpServer;
450
+ // ... (existing logic to build mcpServer from serverRaw)
425
451
  if (serverRaw.mode === 'sse') {
426
452
  mcpServer = {
427
453
  name: serverRaw.name,
@@ -436,7 +462,7 @@ export function formDataToFeedConfiguration(formElement, forExistingCategoryTab
436
462
  requirements: []
437
463
  }
438
464
  };
439
- } else {
465
+ } else { // stdio or other modes
440
466
  mcpServer = {
441
467
  name: serverRaw.name,
442
468
  description: serverRaw.description,
@@ -446,7 +472,7 @@ export function formDataToFeedConfiguration(formElement, forExistingCategoryTab
446
472
  installation: {
447
473
  command: serverRaw.installation.command,
448
474
  args: serverRaw.installation.args || [],
449
- env: {}
475
+ env: {} // Initialize env
450
476
  },
451
477
  dependencies: {
452
478
  requirements: []
@@ -466,7 +492,7 @@ export function formDataToFeedConfiguration(formElement, forExistingCategoryTab
466
492
  if (Object.keys(envVars).length > 0) {
467
493
  mcpServer.installation.env = envVars;
468
494
  } else {
469
- delete mcpServer.installation.env;
495
+ delete mcpServer.installation.env; // Clean up if no env vars
470
496
  }
471
497
  }
472
498
 
@@ -500,6 +526,7 @@ export function formDataToFeedConfiguration(formElement, forExistingCategoryTab
500
526
  assetName: reqRaw.registry.local.assetName,
501
527
  };
502
528
  }
529
+ // Clean up empty registry objects
503
530
  if (fullRequirementConfig.registry.githubRelease && !fullRequirementConfig.registry.githubRelease.repository) delete fullRequirementConfig.registry.githubRelease;
504
531
  if (fullRequirementConfig.registry.artifacts && !fullRequirementConfig.registry.artifacts.registryUrl) delete fullRequirementConfig.registry.artifacts;
505
532
  if (fullRequirementConfig.registry.local && !fullRequirementConfig.registry.local.localPath) delete fullRequirementConfig.registry.local;
@@ -514,17 +541,103 @@ export function formDataToFeedConfiguration(formElement, forExistingCategoryTab
514
541
 
515
542
  const reqKey = `${reqRaw.type}|${reqRaw.name}|${reqRaw.version}`;
516
543
  if (!globalRequirementsMap.has(reqKey)) {
517
- const { order, ...reqConfigForGlobal } = fullRequirementConfig;
544
+ const { order, ...reqConfigForGlobal } = fullRequirementConfig; // Exclude order for global list
518
545
  globalRequirementsMap.set(reqKey, reqConfigForGlobal);
519
546
  }
520
547
  });
521
548
  if (mcpServer.dependencies.requirements.length === 0) {
522
- delete mcpServer.dependencies;
549
+ delete mcpServer.dependencies; // Clean up if no requirements
550
+ }
551
+
552
+ // Attempt to retrieve and add systemTags from the DOM element,
553
+ // ONLY if we are in the 'Create Server in Existing Category' tab context.
554
+ // For 'Create New Category' tab, systemTags should not be assigned from DOM.
555
+ if (formElement.id === 'onboardServerForm' && serversListId) {
556
+ const selector = `#${serversListId} .server-item[data-index="${serverIndex}"]`;
557
+ const serverItemElement = document.querySelector(selector);
558
+ if (serverItemElement && serverItemElement.dataset.systemTags) {
559
+ try {
560
+ mcpServer.systemTags = JSON.parse(serverItemElement.dataset.systemTags);
561
+ } catch (e) {
562
+ console.error(`[formDataToFeedConfiguration] Error parsing systemTags for server index ${serverIndex} ('${mcpServer.name}') on ${formElement.id}:`, e, serverItemElement.dataset.systemTags);
563
+ mcpServer.systemTags = { parseError: true };
564
+ }
565
+ }
566
+ // If no dataset.systemTags or not onboardServerForm, mcpServer.systemTags remains undefined.
567
+ } else {
568
+ // Ensure systemTags is not carried over if not in the correct context or not present in dataset
569
+ delete mcpServer.systemTags;
523
570
  }
524
571
 
525
572
  feedConfiguration.mcpServers.push(mcpServer);
526
573
  });
527
574
 
575
+
576
+ // If processing for an existing category, ensure original non-adhoc servers are included
577
+ // if they weren't picked up by the form (e.g., because they were read-only and disabled).
578
+ if (forExistingCategoryTab && baseCategoryData && Array.isArray(baseCategoryData.mcpServers)) {
579
+ baseCategoryData.mcpServers.forEach(originalServer => {
580
+ // Check if this original server (by name) is already in our processed list.
581
+ // We use name as the primary identifier for existing servers.
582
+ const isAlreadyProcessed = feedConfiguration.mcpServers.some(
583
+ processedServer => processedServer.name === originalServer.name
584
+ );
585
+
586
+ if (!isAlreadyProcessed && (!originalServer.systemTags || originalServer.systemTags.adhoc !== 'true')) {
587
+ // This original server was not in the form data (likely read-only) and is not adhoc. Add it.
588
+ // Ensure its requirements are also added to the global list if not already present.
589
+ feedConfiguration.mcpServers.push(JSON.parse(JSON.stringify(originalServer))); // Add a clone
590
+
591
+ if (originalServer.dependencies && Array.isArray(originalServer.dependencies.requirements)) {
592
+ originalServer.dependencies.requirements.forEach(req => {
593
+ // We need the full requirement definition for the global map.
594
+ // This might require looking up the full definition from baseCategoryData.requirements
595
+ // if originalServer.dependencies.requirements only has name/version/order.
596
+ // For simplicity here, we assume baseCategoryData.requirements contains full definitions.
597
+ const originalGlobalReq = baseCategoryData.requirements?.find(
598
+ gReq => gReq.name === req.name && gReq.type && gReq.version === req.version // Type might not be in server's dep list
599
+ );
600
+
601
+ if (originalGlobalReq) {
602
+ const reqKey = `${originalGlobalReq.type}|${originalGlobalReq.name}|${originalGlobalReq.version}`;
603
+ if (!globalRequirementsMap.has(reqKey)) {
604
+ globalRequirementsMap.set(reqKey, JSON.parse(JSON.stringify(originalGlobalReq)));
605
+ }
606
+ } else {
607
+ // Fallback if full definition not found, add what we have, though type might be missing.
608
+ // This part might need refinement based on actual structure of baseCategoryData.requirements
609
+ // and how server-specific dependencies link to global ones.
610
+ // The current server-specific req usually has name, version, order. Type is global.
611
+ // We need to find the type from the global requirements list.
612
+ // This logic assumes that if a server has a dependency, its full definition (including type)
613
+ // must exist in the global `baseCategoryData.requirements`.
614
+
615
+ // Let's find the type from baseCategoryData.requirements based on name and version
616
+ const matchingGlobalReqForType = baseCategoryData.requirements?.find(
617
+ gReq => gReq.name === req.name && gReq.version === req.version
618
+ );
619
+ if (matchingGlobalReqForType && matchingGlobalReqForType.type) {
620
+ const reqKey = `${matchingGlobalReqForType.type}|${req.name}|${req.version}`;
621
+ if (!globalRequirementsMap.has(reqKey)) {
622
+ // Construct a basic global requirement if not found, though ideally it should exist.
623
+ globalRequirementsMap.set(reqKey, {
624
+ name: req.name,
625
+ version: req.version,
626
+ type: matchingGlobalReqForType.type
627
+ // Other fields like alias, registry would be missing here if not in matchingGlobalReqForType
628
+ });
629
+ }
630
+ } else {
631
+ console.warn(`Could not find full global requirement definition (or type) for ${req.name} v${req.version} from original server ${originalServer.name}`);
632
+ }
633
+ }
634
+ });
635
+ }
636
+ }
637
+ });
638
+ }
639
+
640
+
528
641
  feedConfiguration.requirements = Array.from(globalRequirementsMap.values());
529
642
 
530
643
  // Validate the entire configuration
@@ -668,47 +781,95 @@ export function populateForm(feedConfig, formId = 'onboardForm', renderServersAs
668
781
  clearEnvCountersForTab(serversListId);
669
782
  clearServerRequirementCountersForTab(serversListId);
670
783
 
671
- (feedConfig.mcpServers || []).forEach((server) => {
784
+ (feedConfig.mcpServers || []).forEach((serverData) => { // Renamed 'server' to 'serverData' for clarity
672
785
  // Get the correct current index for this tab before adding the server
673
786
  const currentServerIndex = getServerCounter(serversListId);
674
787
  // window.addServer will increment the counter for serversListId internally
675
- window.addServer(serversListId, renderServersAsReadOnly, server);
676
-
788
+ // It also returns the server item, but we'll query it again for safety after DOM manipulation.
789
+ window.addServer(serversListId, renderServersAsReadOnly, serverData);
790
+
791
+ // After addServer, the server item should exist in the DOM.
792
+ const serverItem = currentForm.querySelector(`#${serversListId} .server-item[data-index="${currentServerIndex}"]`);
793
+
794
+ if (serverItem) {
795
+ if (formId === 'onboardServerForm') {
796
+ if (serverData.systemTags?.adhoc === "true") {
797
+ // Case 1: JSON data explicitly marks it as adhoc. Respect this.
798
+ serverItem.dataset.systemTags = JSON.stringify(serverData.systemTags);
799
+ // console.log(`[populateForm] Server ${serverData.name || currentServerIndex} in ${formId} retains adhoc status from JSON.`);
800
+ } else {
801
+ // Case 2: JSON data does NOT explicitly mark it adhoc.
802
+ // Check if it was an original server from the category.
803
+ // state.originalServerNamesForFormPopulation is set when toggling from JSON to Form view for an existing category.
804
+ if (state.originalServerNamesForFormPopulation && serverData.name && state.originalServerNamesForFormPopulation.has(serverData.name)) {
805
+ // It's an original server from the category, now treated as adhoc because it passed through JSON view.
806
+ serverItem.dataset.systemTags = JSON.stringify({ adhoc: "true" });
807
+ // console.log(`[populateForm] Original server ${serverData.name} in ${formId} marked as adhoc after JSON view.`);
808
+ } else {
809
+ // It's a new server (not in original list) and JSON didn't mark it adhoc.
810
+ // Or, it's an original server but its JSON representation explicitly removed/lacked adhoc tag.
811
+ // Ensure it's NOT adhoc.
812
+ // If serverData.systemTags exists but doesn't have adhoc:true, preserve those other tags.
813
+ if (serverData.systemTags && Object.keys(serverData.systemTags).length > 0) {
814
+ const newTags = { ...serverData.systemTags };
815
+ delete newTags.adhoc; // Ensure adhoc is not true
816
+ if (Object.keys(newTags).length > 0) {
817
+ serverItem.dataset.systemTags = JSON.stringify(newTags);
818
+ } else {
819
+ delete serverItem.dataset.systemTags;
820
+ }
821
+ } else {
822
+ delete serverItem.dataset.systemTags;
823
+ }
824
+ // console.log(`[populateForm] Server ${serverData.name || currentServerIndex} in ${formId} is NOT marked adhoc.`);
825
+ }
826
+ }
827
+ } else {
828
+ // For 'onboardForm' (Create Category tab), or if serverData had systemTags not making it adhoc.
829
+ // If serverData has systemTags, reflect them. Otherwise, ensure no systemTags.
830
+ if (serverData.systemTags) {
831
+ serverItem.dataset.systemTags = JSON.stringify(serverData.systemTags);
832
+ } else {
833
+ delete serverItem.dataset.systemTags;
834
+ }
835
+ }
836
+ }
837
+ // Continue with populating fields using serverData
677
838
  const serverNameInput = currentForm.querySelector(`[name="servers[${currentServerIndex}].name"]`);
678
- if (serverNameInput) serverNameInput.value = server.name || '';
839
+ if (serverNameInput) serverNameInput.value = serverData.name || '';
679
840
 
680
841
  const modeInput = currentForm.querySelector(`[name="servers[${currentServerIndex}].mode"]`);
681
842
  if (modeInput) {
682
- modeInput.value = server.mode || 'stdio';
843
+ modeInput.value = serverData.mode || 'stdio';
683
844
  if (typeof window.renderInstallationConfig === 'function') {
684
- window.renderInstallationConfig(currentServerIndex, serversListId, server.mode || 'stdio', renderServersAsReadOnly, server.installation);
845
+ window.renderInstallationConfig(currentServerIndex, serversListId, serverData.mode || 'stdio', renderServersAsReadOnly, serverData.installation);
685
846
  }
686
847
  }
687
848
  const descInput = currentForm.querySelector(`[name="servers[${currentServerIndex}].description"]`);
688
- if (descInput) descInput.value = server.description || '';
849
+ if (descInput) descInput.value = serverData.description || '';
689
850
 
690
- if (server.schemas) {
851
+ if (serverData.schemas) {
691
852
  const schemaPathEl = document.getElementById(`schema-path-${currentServerIndex}`);
692
- if (schemaPathEl) schemaPathEl.value = server.schemas;
853
+ if (schemaPathEl) schemaPathEl.value = serverData.schemas;
693
854
  }
694
- if (server.repository) {
855
+ if (serverData.repository) {
695
856
  const repoInput = currentForm.querySelector(`[name="servers[${currentServerIndex}].repository"]`);
696
- if (repoInput) repoInput.value = server.repository;
857
+ if (repoInput) repoInput.value = serverData.repository;
697
858
  }
698
859
 
699
- if (server.installation) {
700
- if (server.mode === 'sse') {
860
+ if (serverData.installation) {
861
+ if (serverData.mode === 'sse') {
701
862
  const urlInput = currentForm.querySelector(`[name="servers[${currentServerIndex}].installation.url"]`);
702
- if (urlInput) urlInput.value = server.installation.url || '';
863
+ if (urlInput) urlInput.value = serverData.installation.url || '';
703
864
  } else {
704
865
  const cmdInput = currentForm.querySelector(`[name="servers[${currentServerIndex}].installation.command"]`);
705
- if (cmdInput) cmdInput.value = server.installation.command || '';
706
- if (server.installation.args && Array.isArray(server.installation.args)) {
866
+ if (cmdInput) cmdInput.value = serverData.installation.command || '';
867
+ if (serverData.installation.args && Array.isArray(serverData.installation.args)) {
707
868
  const argsInput = currentForm.querySelector(`[name="servers[${currentServerIndex}].installation.args"]`);
708
- if (argsInput) argsInput.value = server.installation.args.join(', ');
869
+ if (argsInput) argsInput.value = serverData.installation.args.join(', ');
709
870
  }
710
- if (server.installation.env) {
711
- Object.entries(server.installation.env).forEach(([envName, envConfig]) => {
871
+ if (serverData.installation.env) {
872
+ Object.entries(serverData.installation.env).forEach(([envName, envConfig]) => {
712
873
  const currentEnvIndex = window.addEnvVariable(currentServerIndex, serversListId, renderServersAsReadOnly);
713
874
 
714
875
  const nameInput = currentForm.querySelector(`[name="servers[${currentServerIndex}].installation.env[${currentEnvIndex}].name"]`);
@@ -730,8 +891,12 @@ export function populateForm(feedConfig, formId = 'onboardForm', renderServersAs
730
891
  }
731
892
  }
732
893
 
733
- if (server.dependencies && server.dependencies.requirements) {
734
- server.dependencies.requirements.forEach((depReq) => {
894
+ if (serverData.dependencies && serverData.dependencies.requirements) {
895
+ // Determine if this server's requirements should be effectively read-only
896
+ const serverIsEffectivelyReadOnlyForReqs = renderServersAsReadOnly && !(serverData.systemTags?.adhoc === 'true');
897
+ // console.log(`[populateForm] Server: ${serverData.name}, Adhoc: ${serverData.systemTags?.adhoc === 'true'}, renderServersAsReadOnly: ${renderServersAsReadOnly}, Calculated serverIsEffectivelyReadOnlyForReqs: ${serverIsEffectivelyReadOnlyForReqs}`); // DEBUG
898
+
899
+ serverData.dependencies.requirements.forEach((depReq) => {
735
900
  // Find requirement by name only, as requested.
736
901
  const fullReq = (feedConfig.requirements || []).find(r => r.name === depReq.name);
737
902
  if (!fullReq) {
@@ -739,7 +904,8 @@ export function populateForm(feedConfig, formId = 'onboardForm', renderServersAs
739
904
  return;
740
905
  }
741
906
 
742
- const currentReqIndex = window.addServerRequirement(currentServerIndex, serversListId, renderServersAsReadOnly);
907
+ // Pass serverIsEffectivelyReadOnlyForReqs to addServerRequirement
908
+ const currentReqIndex = window.addServerRequirement(currentServerIndex, serversListId, serverIsEffectivelyReadOnlyForReqs);
743
909
 
744
910
  currentForm.querySelector(`[name="servers[${currentServerIndex}].requirements[${currentReqIndex}].name"]`).value = fullReq.name || '';
745
911
  currentForm.querySelector(`[name="servers[${currentServerIndex}].requirements[${currentReqIndex}].type"]`).value = fullReq.type || '';
@@ -293,11 +293,11 @@ document.addEventListener('DOMContentLoaded', async () => {
293
293
  // addRequirementBtn.addEventListener('click', addRequirement); // addRequirement from uiHandlers.js
294
294
  // }
295
295
 
296
- const addServerBtnNewCategory = document.getElementById('addServerBtn'); // For "Create New Category" tab
296
+ const addServerBtnNewCategory = document.getElementById('addServerBtnNewCategory'); // For "Create New Category" tab
297
297
  if (addServerBtnNewCategory) {
298
298
  // addServer in uiHandlers defaults serversListId to 'serversList', which is correct for this button.
299
299
  // It also defaults isReadOnly to false.
300
- addServerBtnNewCategory.addEventListener('click', () => addServer());
300
+ addServerBtnNewCategory.addEventListener('click', () => window.addServer('serversList', false, null));
301
301
  }
302
302
 
303
303
  const addServerBtnExistingCategory = document.getElementById('addServerToExistingCategoryBtn'); // For "Create Server in Existing Category" tab
@@ -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' : '';