imcp 0.0.10 → 0.0.12

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 (41) hide show
  1. package/README.md +4 -0
  2. package/dist/cli/commands/serve.js +2 -1
  3. package/dist/cli/commands/uninstall.js +14 -6
  4. package/dist/core/ConfigurationLoader.d.ts +1 -1
  5. package/dist/core/ConfigurationLoader.js +25 -4
  6. package/dist/core/ConfigurationProvider.d.ts +2 -1
  7. package/dist/core/ConfigurationProvider.js +69 -5
  8. package/dist/core/MCPManager.d.ts +1 -1
  9. package/dist/core/MCPManager.js +28 -6
  10. package/dist/core/ServerSchemaLoader.d.ts +11 -0
  11. package/dist/core/ServerSchemaLoader.js +43 -0
  12. package/dist/core/ServerSchemaProvider.d.ts +17 -0
  13. package/dist/core/ServerSchemaProvider.js +115 -0
  14. package/dist/core/constants.d.ts +11 -1
  15. package/dist/core/installers/clients/ExtensionInstaller.js +3 -0
  16. package/dist/core/types.d.ts +6 -0
  17. package/dist/services/ServerService.d.ts +7 -1
  18. package/dist/services/ServerService.js +24 -9
  19. package/dist/utils/osUtils.js +1 -1
  20. package/dist/web/public/css/detailsWidget.css +110 -0
  21. package/dist/web/public/css/modal.css +42 -0
  22. package/dist/web/public/css/serverDetails.css +42 -30
  23. package/dist/web/public/js/detailsWidget.js +225 -12
  24. package/dist/web/public/js/modal.js +93 -29
  25. package/dist/web/public/js/notifications.js +34 -35
  26. package/dist/web/server.js +13 -2
  27. package/package.json +1 -1
  28. package/src/cli/commands/serve.ts +4 -3
  29. package/src/cli/commands/uninstall.ts +16 -6
  30. package/src/core/ConfigurationLoader.ts +25 -4
  31. package/src/core/ConfigurationProvider.ts +75 -6
  32. package/src/core/MCPManager.ts +34 -9
  33. package/src/core/constants.ts +12 -2
  34. package/src/core/installers/clients/ExtensionInstaller.ts +3 -0
  35. package/src/core/types.ts +7 -1
  36. package/src/services/ServerService.ts +26 -8
  37. package/src/utils/osUtils.ts +1 -1
  38. package/src/web/public/css/modal.css +42 -0
  39. package/src/web/public/js/modal.js +93 -29
  40. package/src/web/public/js/notifications.js +34 -35
  41. package/src/web/server.ts +21 -3
@@ -141,14 +141,16 @@ function _appendInstallLoadingMessage(message) {
141
141
  /**
142
142
  * Display the installation loading modal and prepare it for messages.
143
143
  */
144
- function showInstallLoadingModal() {
144
+ function showInstallLoadingModal(operation = 'Installing') {
145
145
  const loadingModal = document.getElementById('installLoadingModal');
146
146
  const loadingMsg = document.getElementById('installLoadingMessage');
147
- if (loadingModal && loadingMsg) {
147
+ const loadingTitle = document.querySelector('.loading-title');
148
+ if (loadingModal && loadingMsg && loadingTitle) {
148
149
  loadingModal.style.display = 'block';
149
150
  loadingMsg.innerHTML = '';
151
+ loadingTitle.textContent = `${operation}...`;
150
152
  } else {
151
- console.error('[LoadingModal] Required elements not found: installLoadingModal or installLoadingMessage');
153
+ console.error('[LoadingModal] Required elements not found: installLoadingModal, installLoadingMessage, or loading-title');
152
154
  }
153
155
  }
154
156
 
@@ -330,17 +332,55 @@ async function showInstallModal(categoryName, serverName, callback) {
330
332
  // Add elements to client info
331
333
  clientInfo.appendChild(clientName);
332
334
 
333
- // Add client info (name) to the item first
335
+ // Add elements to client item
334
336
  clientItem.appendChild(clientInfo);
335
337
 
338
+ // Status container for badge and uninstall button
339
+ const statusContainer = document.createElement('div');
340
+ statusContainer.className = 'status-container';
341
+
336
342
  // Status badge - only show if we have status text
337
343
  if (statusText) {
338
344
  const statusBadge = document.createElement('span');
339
345
  statusBadge.className = `status-badge ${statusClass}`;
340
346
  statusBadge.textContent = statusText;
341
- clientItem.appendChild(statusBadge);
347
+ statusContainer.appendChild(statusBadge);
348
+
349
+ // Add uninstall button right after status badge if installed
350
+ if (operationStatus.status === 'completed' && operationStatus.type === 'install') {
351
+ const uninstallBtn = document.createElement('button');
352
+ uninstallBtn.className = 'uninstall-btn text-red-600 hover:text-red-800 ml-2';
353
+ uninstallBtn.innerHTML = '<i class="bx bx-trash"></i>';
354
+ uninstallBtn.title = 'Uninstall from this client';
355
+ uninstallBtn.onclick = async (e) => {
356
+ e.stopPropagation(); // Prevent item selection
357
+ e.preventDefault(); // Prevent form submission
358
+ const confirmed = await showConfirm('Uninstall Confirmation', `Are you sure you want to uninstall ${serverName} from ${target}?`);
359
+ if (confirmed) {
360
+ // Add target to selectedClients for uninstallation
361
+ window.selectedClients = [target];
362
+ showInstallLoadingModal('Uninstalling');
363
+ const serverList = {
364
+ [serverName]: {
365
+ removeData: true // Include removal of associated data
366
+ }
367
+ };
368
+ try {
369
+ delayedAppendInstallLoadingMessage(`Uninstalling ${serverName} from ${target}...`);
370
+ await uninstallTools(categoryName, serverList, [target]);
371
+ } catch (error) {
372
+ delayedAppendInstallLoadingMessage(`Error: ${error.message}`);
373
+ throw error; // Re-throw to trigger error handling in uninstallTools
374
+ }
375
+ }
376
+ return false; // Prevent form submission
377
+ };
378
+ statusContainer.appendChild(uninstallBtn);
379
+ }
342
380
  }
343
381
 
382
+ clientItem.appendChild(statusContainer);
383
+
344
384
  // Add client item to target div
345
385
  targetDiv.appendChild(clientItem);
346
386
  });
@@ -641,34 +681,40 @@ async function showInstallModal(categoryName, serverName, callback) {
641
681
  });
642
682
  });
643
683
 
644
- // Get selected clients
645
- const selectedTargets = window.selectedClients.length > 0 ?
646
- window.selectedClients :
647
- Array.from(document.querySelectorAll('.client-item.selected'))
648
- .map(item => item.dataset.target);
649
-
650
684
  // Check if we have any requirements selected for update
651
685
  const hasRequirementsToUpdate = requirementsToUpdate.length > 0;
652
686
 
653
- // Only require client selection if we don't have any requirements to update
654
- if (selectedTargets.length === 0 && !hasRequirementsToUpdate) {
655
- showToast('Please select at least one client to configure.', 'error');
656
- return;
687
+ // Only proceed if this isn't an uninstall operation
688
+ const uninstallBtn = document.querySelector('.uninstall-btn');
689
+ if (!uninstallBtn || !uninstallBtn.matches(':active')) {
690
+ // Get selected clients
691
+ const selectedTargets = window.selectedClients.length > 0 ?
692
+ window.selectedClients :
693
+ Array.from(document.querySelectorAll('.client-item.selected'))
694
+ .map(item => item.dataset.target);
695
+
696
+ console.log('Selected targets:', selectedTargets);
697
+ console.log('Requirements to update:', requirementsToUpdate);
698
+ if (selectedTargets.length === 0 && !hasRequirementsToUpdate) {
699
+ showToast('Please select at least one client to configure.', 'error');
700
+ return;
701
+ }
702
+ window.selectedClients = selectedTargets;
657
703
  }
658
704
 
659
705
  // Call install function with selected targets
660
706
  // Find installing message for the first selected target
661
707
  let installingMessage = "Starting installation...";
662
708
  const serverStatus = serverStatuses[serverName] || { installedStatus: {} };
663
- if (selectedTargets.length > 0) {
664
- const target = selectedTargets[0];
709
+ if (window.selectedClients.length > 0) {
710
+ const target = window.selectedClients[0];
665
711
  const msg = serverStatus.installedStatus?.[target]?.message;
666
712
  if (msg) installingMessage = msg;
667
713
  }
668
714
 
669
715
  // Add requirements to update to serverInstallOptions if any
670
716
  const serverInstallOptions = {
671
- targetClients: selectedTargets,
717
+ targetClients: window.selectedClients,
672
718
  env: envVars,
673
719
  args: args,
674
720
  settings: pythonEnv ? { pythonEnv } : undefined
@@ -679,7 +725,9 @@ async function showInstallModal(categoryName, serverName, callback) {
679
725
  serverInstallOptions.requirements = requirementsToUpdate;
680
726
  }
681
727
 
682
- handleBulkClientInstall(categoryName, serverName, selectedTargets, envVars, installingMessage, serverData, serverInstallOptions);
728
+ // For installation, use the selectedTargets from the validation above
729
+ const targetsToUse = document.querySelector('.uninstall-btn:hover') ? [] : window.selectedClients;
730
+ handleBulkClientInstall(categoryName, serverName, targetsToUse, envVars, installingMessage, serverData, serverInstallOptions);
683
731
  };
684
732
 
685
733
  } catch (error) {
@@ -893,23 +941,35 @@ async function pollInstallStatus(categoryName, serverName, targets, interval = 2
893
941
 
894
942
  // Function to handle client uninstallation for multiple targets
895
943
  async function uninstallTools(categoryName, serverList, targets) {
896
- if (!Array.isArray(targets)) {
897
- targets = [targets]; // Convert single target to array for backward compatibility
898
- }
899
-
900
- const confirmed = await showConfirm(`Are you sure you want to uninstall this server for ${targets.length} client(s)?`);
901
- if (!confirmed) {
944
+ // Get selected targets from window.selectedClients or the provided targets
945
+ const selectedTargets = window.selectedClients || (Array.isArray(targets) ? targets : [targets]);
946
+
947
+ // Validate selected targets
948
+ if (!selectedTargets || selectedTargets.length === 0) {
949
+ showToast('Please select at least one client to uninstall.', 'error');
902
950
  return;
903
951
  }
904
952
 
905
953
  try {
954
+ delayedAppendInstallLoadingMessage('Starting uninstallation...');
955
+
956
+ // Ensure serverList is an object where each key is a server name
957
+ if (Array.isArray(serverList)) {
958
+ const formattedServerList = {};
959
+ serverList.forEach(server => {
960
+ formattedServerList[server] = { removeData: true };
961
+ });
962
+ serverList = formattedServerList;
963
+ }
964
+
906
965
  const response = await fetch(`/api/categories/${categoryName}/uninstall`, {
907
966
  method: 'POST',
908
967
  headers: { 'Content-Type': 'application/json' },
909
968
  body: JSON.stringify({
910
- toolList: serverList,
969
+ serverList: serverList,
911
970
  options: {
912
- targets: targets
971
+ targets: selectedTargets,
972
+ removeData: true
913
973
  }
914
974
  })
915
975
  });
@@ -924,10 +984,14 @@ async function uninstallTools(categoryName, serverList, targets) {
924
984
  throw new Error(result.error || 'Uninstallation failed');
925
985
  }
926
986
 
927
- showToast(`Successfully uninstalled for ${targets.length} client(s).`, 'success');
928
- location.reload(); // Refresh the page to update the UI
987
+ // Add completion message and trigger completion UI
988
+ delayedAppendInstallLoadingMessage(`Successfully uninstalled from ${selectedTargets.join(', ')}`, true);
989
+
990
+ // Clear selected clients after successful uninstall
991
+ window.selectedClients = [];
929
992
  } catch (error) {
930
993
  console.error('Error uninstalling tools:', error);
994
+ delayedAppendInstallLoadingMessage(`Error: ${error.message}`, true);
931
995
  showToast(`Error uninstalling tools: ${error.message}`, 'error');
932
996
  }
933
997
  }
@@ -30,28 +30,26 @@ function showToast(message, type = 'success') {
30
30
  });
31
31
  }
32
32
 
33
- // Function to show a Bootstrap confirmation dialog
34
- function showConfirm(message) {
33
+ // Function to show a confirmation dialog using custom modal system
34
+ function showConfirm(title, message) {
35
35
  return new Promise((resolve) => {
36
36
  const modalId = `confirm-modal-${Date.now()}`;
37
37
  const modalHtml = `
38
- <div class="modal fade" id="${modalId}" tabindex="-1" aria-labelledby="${modalId}-label" aria-hidden="true">
39
- <div class="modal-dialog">
40
- <div class="modal-content">
41
- <div class="modal-header">
42
- <h5 class="modal-title" id="${modalId}-label">Confirm Action</h5>
43
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
44
- </div>
45
- <div class="modal-body">
46
- ${message}
47
- </div>
48
- <div class="modal-footer">
49
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
50
- <button type="button" class="btn btn-primary confirm-action">
51
- <span class="spinner-border spinner-border-sm d-none me-2" role="status" aria-hidden="true"></span>
52
- Confirm
53
- </button>
54
- </div>
38
+ <div id="${modalId}" class="modal">
39
+ <div class="modal-content" style="max-width: 400px; margin: 15% auto;">
40
+ <div class="modal-header">
41
+ <h3 class="text-lg font-semibold text-gray-700">${title}</h3>
42
+ </div>
43
+ <div style="margin: 1rem 0;">
44
+ ${message}
45
+ </div>
46
+ <div style="display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 1rem;">
47
+ <button type="button" class="cancel-button px-4 py-2 text-gray-600 hover:text-gray-800 font-medium rounded-lg hover:bg-gray-100 transition-colors">
48
+ Cancel
49
+ </button>
50
+ <button type="button" class="confirm-button submit-button">
51
+ Confirm
52
+ </button>
55
53
  </div>
56
54
  </div>
57
55
  </div>
@@ -60,29 +58,30 @@ function showConfirm(message) {
60
58
  document.body.insertAdjacentHTML('beforeend', modalHtml);
61
59
 
62
60
  const modalElement = document.getElementById(modalId);
63
- const modal = new bootstrap.Modal(modalElement);
61
+ modalElement.style.display = 'block';
64
62
 
65
63
  // Handle confirm button click
66
- modalElement.querySelector('.confirm-action').addEventListener('click', (event) => {
67
- const button = event.currentTarget;
68
- const spinner = button.querySelector('.spinner-border');
69
- button.disabled = true;
70
- spinner.classList.remove('d-none');
71
-
72
- // Add small delay to show the loading state
73
- setTimeout(() => {
74
- modal.hide();
75
- resolve(true);
76
- }, 300);
64
+ modalElement.querySelector('.confirm-button').addEventListener('click', () => {
65
+ modalElement.style.display = 'none';
66
+ modalElement.remove();
67
+ resolve(true);
77
68
  });
78
69
 
79
- // Handle modal hidden event
80
- modalElement.addEventListener('hidden.bs.modal', () => {
70
+ // Handle cancel button click
71
+ modalElement.querySelector('.cancel-button').addEventListener('click', () => {
72
+ modalElement.style.display = 'none';
81
73
  modalElement.remove();
82
74
  resolve(false);
83
75
  });
84
-
85
- modal.show();
76
+
77
+ // Handle click outside modal
78
+ modalElement.addEventListener('click', (event) => {
79
+ if (event.target === modalElement) {
80
+ modalElement.style.display = 'none';
81
+ modalElement.remove();
82
+ resolve(false);
83
+ }
84
+ });
86
85
  });
87
86
  }
88
87
 
@@ -110,13 +110,24 @@ app.post('/api/categories/:categoryName/uninstall', async (req, res) => {
110
110
  try {
111
111
  const { categoryName } = req.params;
112
112
  const { serverList } = req.body;
113
- if (!Array.isArray(serverList) || serverList.length === 0) {
113
+ if (!serverList || Object.keys(serverList).length === 0) {
114
114
  return res.status(400).json({
115
115
  success: false,
116
116
  error: 'Invalid tool list provided'
117
117
  });
118
118
  }
119
- const results = await Promise.all(serverList.map(serverName => serverService.uninstallMcpServer(categoryName, serverName)));
119
+ const { options } = req.body;
120
+ if (!options?.targets || options.targets.length === 0) {
121
+ return res.status(400).json({
122
+ success: false,
123
+ error: 'No target clients specified'
124
+ });
125
+ }
126
+ const results = await Promise.all(Object.entries(serverList).map(([serverName, serverOptions]) => serverService.uninstallMcpServer(categoryName, serverName, {
127
+ ...serverOptions,
128
+ targets: options.targets,
129
+ removeData: options.removeData ?? serverOptions.removeData
130
+ })));
120
131
  const { success, messages } = serverService.formatOperationResults(results);
121
132
  const response = {
122
133
  success,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imcp",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Node.js SDK for Model Context Protocol (MCP)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,21 +8,22 @@ export function createServeCommand(): Command {
8
8
  return new Command('serve')
9
9
  .description('Serve local web interface')
10
10
  .option('-p, --port <port>', 'Port to run the server on', '3000')
11
+ .option('-f, --feed-file <filepath>', 'Path to a custom feed configuration file')
11
12
  .action(async (options) => {
12
13
  try {
13
14
  // Sync feeds before start the local UI
14
15
  await mcpManager.syncFeeds();
15
16
 
16
17
  // Ensure MCP manager is initialized before starting the web server
17
- await mcpManager.initialize();
18
-
18
+ await mcpManager.initialize(options.feedFile);
19
+
19
20
  const port = parseInt(options.port, 10);
20
21
  if (isNaN(port) || port < 1 || port > 65535) {
21
22
  throw new Error('Invalid port number');
22
23
  }
23
24
 
24
25
  await startWebServer(port);
25
-
26
+
26
27
  // The server is running, keep the process alive
27
28
  process.on('SIGINT', () => {
28
29
  console.log('\nShutting down server...');
@@ -6,19 +6,27 @@ export function createUninstallCommand(): Command {
6
6
  .description('Uninstall specific MCP servers')
7
7
  .requiredOption(
8
8
  '--category <category>',
9
+ 'Server category'
10
+ )
11
+ .requiredOption(
9
12
  '--names <names>',
10
13
  'Server names (semicolon separated)'
11
14
  )
15
+ .requiredOption(
16
+ '--targets <targets>',
17
+ 'Target clients to uninstall from (semicolon separated)'
18
+ )
12
19
  .option(
13
20
  '--remove-data',
14
21
  'Remove all associated data',
15
- false
22
+ true // Change default to true to ensure cleanup happens
16
23
  )
17
24
  .action(async (options) => {
18
25
  try {
19
26
  const serverNames = options.names.split(';').map((name: string) => name.trim());
20
27
 
21
- if (!serverService.validateServerName(options.category, serverNames)) {
28
+ const validNames = await serverService.validateServerName(options.category, serverNames);
29
+ if (!validNames) {
22
30
  console.error('Invalid server names provided');
23
31
  process.exit(1);
24
32
  }
@@ -26,11 +34,13 @@ export function createUninstallCommand(): Command {
26
34
  console.log(`Uninstalling servers: ${serverNames.join(', ')}`);
27
35
 
28
36
  const results = await Promise.all(
29
- serverNames.map((serverName: string) =>
30
- serverService.uninstallMcpServer(options.category, serverName, {
37
+ serverNames.map((serverName: string) => {
38
+ const targets = options.targets ? options.targets.split(';').map((t: string) => t.trim()) : [];
39
+ return serverService.uninstallMcpServer(options.category, serverName, {
31
40
  removeData: options.removeData,
32
- })
33
- )
41
+ targets: targets
42
+ });
43
+ })
34
44
  );
35
45
 
36
46
  const { success, messages } = serverService.formatOperationResults(results);
@@ -177,25 +177,46 @@ export class ConfigurationLoader {
177
177
  /**
178
178
  * Loads feed configurations into the MCP configuration
179
179
  */
180
- static async loadFeedsIntoConfiguration(configuration: MCPConfiguration): Promise<MCPConfiguration> {
180
+ static async loadFeedsIntoConfiguration(configuration: MCPConfiguration, feedFile?: string): Promise<MCPConfiguration> {
181
181
  try {
182
182
  await fs.mkdir(LOCAL_FEEDS_DIR, { recursive: true });
183
+ const feeds: Record<string, FeedConfiguration> = {};
184
+
185
+ // Load provided feed file if specified
186
+ if (feedFile) {
187
+ try {
188
+ const content = await fs.readFile(feedFile, 'utf8');
189
+ const config = JSON.parse(content) as FeedConfiguration;
190
+ if (config && config.name) {
191
+ feeds[config.name] = config;
192
+ console.log(`Loaded feed configuration from provided file: ${feedFile}`);
193
+ }
194
+ } catch (error) {
195
+ console.log(`Error loading feed configuration from provided file ${feedFile}:`, error);
196
+ }
197
+ }
198
+
199
+ // Load feeds from LOCAL_FEEDS_DIR
183
200
  const files = await fs.readdir(LOCAL_FEEDS_DIR);
184
201
  const jsonFiles = files.filter(file => file.endsWith('.json'));
185
202
 
186
- if (jsonFiles.length === 0) {
203
+ if (jsonFiles.length === 0 && !feedFile) {
187
204
  console.log(`No feed configuration files found in ${LOCAL_FEEDS_DIR}`);
188
205
  return configuration;
189
206
  }
190
207
 
191
- const feeds: Record<string, FeedConfiguration> = {};
192
208
  for (const file of jsonFiles) {
193
209
  try {
194
210
  const filePath = path.join(LOCAL_FEEDS_DIR, file);
195
211
  const content = await fs.readFile(filePath, 'utf8');
196
212
  const config = JSON.parse(content) as FeedConfiguration;
197
213
  if (config && config.name) {
198
- feeds[config.name] = config;
214
+ // If feed exists from provided file, skip the local one
215
+ if (!feeds[config.name]) {
216
+ feeds[config.name] = config;
217
+ } else {
218
+ console.log(`Skipping local feed ${config.name} as it was provided via --feed-file`);
219
+ }
199
220
  }
200
221
  } catch (error) {
201
222
  console.warn(`Error loading feed configuration from ${file}:`, error);
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import { exec } from 'child_process';
4
4
  import { promisify } from 'util';
5
5
  import { fileURLToPath } from 'url';
6
- import { GITHUB_REPO, LOCAL_FEEDS_DIR, SETTINGS_DIR } from './constants.js';
6
+ import { GITHUB_REPO, LOCAL_FEEDS_DIR, SETTINGS_DIR, SUPPORTED_CLIENTS } from './constants.js';
7
7
  import { Logger } from '../utils/logger.js';
8
8
  import { checkGithubAuth } from '../utils/githubAuth.js';
9
9
  import {
@@ -13,7 +13,8 @@ import {
13
13
  InstallationStatus,
14
14
  RequirementStatus,
15
15
  MCPServerStatus,
16
- OperationStatus
16
+ OperationStatus,
17
+ ClientSettings
17
18
  } from './types.js';
18
19
  import { ConfigurationLoader } from './ConfigurationLoader.js';
19
20
 
@@ -57,7 +58,7 @@ export class ConfigurationProvider {
57
58
  }
58
59
  }
59
60
 
60
- async initialize(): Promise<void> {
61
+ async initialize(feedFile?: string): Promise<void> {
61
62
  await this.withLock(async () => {
62
63
  const configDir = path.dirname(this.configPath);
63
64
  await fs.mkdir(configDir, { recursive: true });
@@ -75,7 +76,7 @@ export class ConfigurationProvider {
75
76
  }
76
77
 
77
78
  // Always load feeds and client settings, whether file existed or not
78
- await this.loadFeedsIntoConfiguration();
79
+ await this.loadFeedsIntoConfiguration(feedFile);
79
80
  await this.loadClientMCPSettings();
80
81
  } catch (error) {
81
82
  Logger.error('Error during initialization', error);
@@ -340,8 +341,8 @@ export class ConfigurationProvider {
340
341
  });
341
342
  }
342
343
 
343
- private async loadFeedsIntoConfiguration(): Promise<void> {
344
- this.configuration = await ConfigurationLoader.loadFeedsIntoConfiguration(this.configuration);
344
+ private async loadFeedsIntoConfiguration(feedFile?: string): Promise<void> {
345
+ this.configuration = await ConfigurationLoader.loadFeedsIntoConfiguration(this.configuration, feedFile);
345
346
  await this.saveConfiguration();
346
347
  }
347
348
 
@@ -356,6 +357,74 @@ export class ConfigurationProvider {
356
357
  await this.loadClientMCPSettings();
357
358
  });
358
359
  }
360
+
361
+ async removeServerFromClientMCPSettings(serverName: string, target?: string): Promise<void> {
362
+ return await this.withLock(async () => {
363
+ // Load utils in async context to avoid circular dependencies
364
+ const { readJsonFile, writeJsonFile } = await import('../utils/clientUtils.js');
365
+
366
+ // Filter clients if target is specified
367
+ const clientEntries = Object.entries(SUPPORTED_CLIENTS as Record<string, ClientSettings>);
368
+ const targetClients = target
369
+ ? clientEntries.filter(([clientName]) => clientName === target)
370
+ : clientEntries;
371
+
372
+ for (const [clientName, clientSettings] of targetClients) {
373
+ const settingPath = process.env.CODE_INSIDERS
374
+ ? clientSettings.codeInsiderSettingPath
375
+ : clientSettings.codeSettingPath;
376
+
377
+ try {
378
+ const content = await readJsonFile(settingPath, true);
379
+ let modified = false;
380
+
381
+ // Handle GitHub Copilot's different structure
382
+ if (clientName === 'GithubCopilot' && content.mcp?.servers?.[serverName]) {
383
+ delete content.mcp.servers[serverName];
384
+ modified = true;
385
+ } else if (content.mcpServers?.[serverName]) {
386
+ delete content.mcpServers[serverName];
387
+ modified = true;
388
+ }
389
+
390
+ // Only write if we actually modified the content
391
+ if (modified) {
392
+ await writeJsonFile(settingPath, content);
393
+ Logger.debug(`Removed server ${serverName} from client ${clientName} settings`);
394
+ }
395
+ } catch (error) {
396
+ Logger.error(`Failed to remove server ${serverName} from client ${clientName} settings:`, error);
397
+ }
398
+ }
399
+
400
+ // Also update our in-memory configuration
401
+ if (this.configuration.clientMCPSettings) {
402
+ if (target) {
403
+ // Only update settings for the target client
404
+ const clientSettings = this.configuration.clientMCPSettings[target];
405
+ if (clientSettings) {
406
+ if (clientSettings.mcpServers?.[serverName]) {
407
+ delete clientSettings.mcpServers[serverName];
408
+ }
409
+ if (clientSettings.servers?.[serverName]) { // For GitHub Copilot
410
+ delete clientSettings.servers[serverName];
411
+ }
412
+ }
413
+ } else {
414
+ // Update all clients if no target specified
415
+ for (const clientSettings of Object.values(this.configuration.clientMCPSettings)) {
416
+ if (clientSettings.mcpServers?.[serverName]) {
417
+ delete clientSettings.mcpServers[serverName];
418
+ }
419
+ if (clientSettings.servers?.[serverName]) { // For GitHub Copilot
420
+ delete clientSettings.servers[serverName];
421
+ }
422
+ }
423
+ }
424
+ await this.saveConfiguration();
425
+ }
426
+ });
427
+ }
359
428
  }
360
429
 
361
430
  // Export a singleton instance
@@ -9,8 +9,7 @@ import {
9
9
  ServerCategoryListOptions,
10
10
  ServerOperationResult,
11
11
  ServerUninstallOptions,
12
- InstallationStatus,
13
- UpdateRequirementOptions
12
+ InstallationStatus
14
13
  } from './types.js';
15
14
  import path from 'path';
16
15
 
@@ -28,9 +27,9 @@ export class MCPManager extends EventEmitter {
28
27
  await this.configProvider.syncFeeds();
29
28
  }
30
29
 
31
- async initialize(): Promise<void> {
30
+ async initialize(feedFile?: string): Promise<void> {
32
31
  try {
33
- await this.configProvider.initialize();
32
+ await this.configProvider.initialize(feedFile);
34
33
  } catch (error) {
35
34
  console.error("Error during MCPManager initialization:", error);
36
35
  throw error;
@@ -95,18 +94,44 @@ export class MCPManager extends EventEmitter {
95
94
  };
96
95
  }
97
96
 
98
- // Clear installation status
97
+ const { targets = [], removeData = false } = options;
98
+
99
+ // Clear installation status for specified targets
100
+ const currentStatus: InstallationStatus = serverCategory.installationStatus || {
101
+ requirementsStatus: {},
102
+ serversStatus: {},
103
+ lastUpdated: new Date().toISOString()
104
+ };
105
+ const serversStatus = currentStatus.serversStatus || {};
106
+ const serverStatus = serversStatus[serverName] || { installedStatus: {}, name: serverName };
107
+
108
+ // Only reset installedStatus for specified targets
109
+ for (const target of targets) {
110
+ if (serverStatus.installedStatus) {
111
+ delete serverStatus.installedStatus[target];
112
+ }
113
+ }
114
+ if (removeData) {
115
+ for (const target of targets) {
116
+ await this.configProvider.removeServerFromClientMCPSettings(serverName, target);
117
+ }
118
+ }
119
+
120
+ // Update server status
121
+ serversStatus[serverName] = serverStatus;
122
+
123
+ // Update status keeping requirements
99
124
  await this.configProvider.updateInstallationStatus(
100
125
  categoryName,
101
- {},
102
- {}
126
+ currentStatus.requirementsStatus || {},
127
+ serversStatus
103
128
  );
104
129
 
105
- this.emit(MCPEvent.SERVER_UNINSTALLED, { serverName });
130
+ this.emit(MCPEvent.SERVER_UNINSTALLED, { serverName, targets });
106
131
 
107
132
  return {
108
133
  success: true,
109
- message: `Successfully uninstalled ${serverName}`,
134
+ message: `Successfully uninstalled ${serverName} from ${targets.join(', ')}`,
110
135
  };
111
136
  } catch (error) {
112
137
  return {