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
@@ -58,8 +58,18 @@ const CODE_INSIDER_STRORAGE_DIR = (() => {
58
58
  * Value: Client-specific settings or configuration details.
59
59
  * TODO: Define actual client settings structure.
60
60
  */
61
- export const SUPPORTED_CLIENTS: Record<string, any> = {
62
- 'MSRooCode': { /* MSROO specific settings */
61
+ export const SUPPORTED_CLIENTS: Record<string, {
62
+ extension: {
63
+ extensionId: string;
64
+ leastVersion?: string;
65
+ repository?: string;
66
+ assetName?: string;
67
+ private?: boolean;
68
+ };
69
+ codeSettingPath: string;
70
+ codeInsiderSettingPath: string;
71
+ }> = {
72
+ 'MSRooCode': {
63
73
 
64
74
  extension: {
65
75
  extensionId: 'microsoftai.ms-roo-cline',
@@ -140,6 +140,9 @@ export class ExtensionInstaller {
140
140
  } else {
141
141
  // Install private extension from GitHub release using latest version
142
142
  try {
143
+ if (!repository || !assetName) {
144
+ throw new Error(`Missing repository or assetName for private extension ${extensionId}`);
145
+ }
143
146
  const { resolvedPath } = await handleGitHubRelease(
144
147
  { name: extensionId, version: 'latest', type: 'extension' },
145
148
  { repository, assetName }
package/src/core/types.ts CHANGED
@@ -84,6 +84,7 @@ export interface UpdateRequirementOptions {
84
84
 
85
85
  export interface ServerUninstallOptions {
86
86
  removeData?: boolean;
87
+ targets?: string[]; // List of client targets to uninstall from
87
88
  }
88
89
 
89
90
  // Types related to server feed configuration (e.g., ai-coder-tools.json)
@@ -150,6 +151,11 @@ export interface FeedConfiguration {
150
151
  mcpServers: McpConfig[];
151
152
  }
152
153
 
154
+ export interface ClientSettings {
155
+ codeSettingPath: string;
156
+ codeInsiderSettingPath: string;
157
+ }
158
+
153
159
  // Events that can be emitted by the SDK
154
160
  export enum MCPEvent {
155
161
  SERVER_INSTALLED = 'server:installed',
@@ -161,7 +167,7 @@ export enum MCPEvent {
161
167
 
162
168
  export interface MCPEventData {
163
169
  [MCPEvent.SERVER_INSTALLED]: { server: MCPServerCategory };
164
- [MCPEvent.SERVER_UNINSTALLED]: { serverName: string };
170
+ [MCPEvent.SERVER_UNINSTALLED]: { serverName: string; targets?: string[] };
165
171
  [MCPEvent.SERVER_STARTED]: { server: MCPServerCategory };
166
172
  [MCPEvent.SERVER_STOPPED]: { serverName: string };
167
173
  [MCPEvent.CONFIG_CHANGED]: { configuration: MCPConfiguration };
@@ -147,19 +147,34 @@ export class ServerService {
147
147
  * Installs a specific mcp tool for a server.
148
148
  * TODO: This might require enhancing MCPManager to handle category-specific installs.
149
149
  */
150
+ /**
151
+ * Uninstall MCP server from specified client targets
152
+ * @param category The server category
153
+ * @param serverName The server name to uninstall
154
+ * @param options Uninstall options including target clients and data removal flags
155
+ */
150
156
  async uninstallMcpServer(
151
157
  category: string,
152
158
  serverName: string,
153
- options: ServerUninstallOptions = {} // Reuse ServerInstallOptions for env etc.
159
+ options: ServerUninstallOptions = {}
154
160
  ): Promise<ServerOperationResult> {
155
- return mcpManager.uninstallServer(category, serverName, options);
161
+ Logger.debug(`Uninstalling MCP server: ${JSON.stringify({ category, serverName, options })}`);
162
+ try {
163
+ const result = await mcpManager.uninstallServer(category, serverName, options);
164
+ Logger.debug(`Uninstallation result: ${JSON.stringify(result)}`);
165
+ return result;
166
+ } catch (error) {
167
+ Logger.error(`Failed to uninstall MCP server: ${serverName}`, error);
168
+ throw error;
169
+ }
156
170
  }
157
171
 
158
172
  /**
159
173
  * Validates server names
160
174
  */
161
- async validateServerName(category: string, name: string): Promise<boolean> {
162
- Logger.debug(`Validating server name: ${JSON.stringify({ category, name })}`);
175
+ async validateServerName(category: string, names: string | string[]): Promise<boolean> {
176
+ const serverNames = Array.isArray(names) ? names : [names];
177
+ Logger.debug(`Validating server names: ${JSON.stringify({ category, names: serverNames })}`);
163
178
 
164
179
  // Check if category exists in feeds
165
180
  const feedConfig = await mcpManager.getFeedConfiguration(category);
@@ -168,10 +183,13 @@ export class ServerService {
168
183
  return false;
169
184
  }
170
185
 
171
- // Check if server exists in the category's mcpServers
172
- const serverExists = feedConfig.mcpServers.some(server => server.name === name);
173
- if (!serverExists) {
174
- Logger.debug(`Validation failed: Server "${name}" not found in category "${category}"`);
186
+ // Check if all servers exist in the category's mcpServers
187
+ const invalidServers = serverNames.filter(name =>
188
+ !feedConfig.mcpServers.some(server => server.name === name)
189
+ );
190
+
191
+ if (invalidServers.length > 0) {
192
+ Logger.debug(`Validation failed: Servers "${invalidServers.join(', ')}" not found in category "${category}"`);
175
193
  return false;
176
194
  }
177
195
 
@@ -273,7 +273,7 @@ export function getPythonPackagePath(pythonExecutablePath: string): string {
273
273
  // Virtual environment structure on Windows: <venv>/Scripts/python.exe
274
274
  const venvRoot = path.dirname(dir);
275
275
  return path.join(venvRoot, 'Lib', 'site-packages');
276
- } else if (dir.toLowerCase().includes('python')) {
276
+ } else {
277
277
  // System Python or Conda on Windows
278
278
  return path.join(dir, 'Lib', 'site-packages');
279
279
  }
@@ -160,6 +160,15 @@ body {
160
160
  transition: all 0.2s ease;
161
161
  cursor: pointer;
162
162
  user-select: none;
163
+ gap: 0.5rem;
164
+ }
165
+
166
+ /* Client actions container */
167
+ .client-actions {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: 0.5rem;
171
+ margin-left: auto;
163
172
  }
164
173
 
165
174
  .client-item:hover {
@@ -180,6 +189,14 @@ body {
180
189
  gap: 0.75rem;
181
190
  }
182
191
 
192
+ /* Status container */
193
+ .status-container {
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 0.5rem;
197
+ margin-left: auto;
198
+ }
199
+
183
200
  /* Status badges */
184
201
  .status-badge {
185
202
  display: inline-flex;
@@ -227,6 +244,31 @@ body {
227
244
  background-color: #fef3c7;
228
245
  }
229
246
 
247
+ /* Uninstall button styling */
248
+ .uninstall-btn {
249
+ display: inline-flex;
250
+ align-items: center;
251
+ justify-content: center;
252
+ width: 28px;
253
+ height: 28px;
254
+ border-radius: 6px;
255
+ border: 1px solid transparent;
256
+ background: transparent;
257
+ cursor: pointer;
258
+ transition: all 0.2s ease;
259
+ padding: 0;
260
+ }
261
+
262
+ .uninstall-btn:hover {
263
+ background-color: #fee2e2;
264
+ border-color: #ef4444;
265
+ transform: scale(1.05);
266
+ }
267
+
268
+ .uninstall-btn i {
269
+ font-size: 1.25rem;
270
+ }
271
+
230
272
  /* Environment variables section */
231
273
  #modalEnvInputs input {
232
274
  width: 100%;
@@ -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
 
package/src/web/server.ts CHANGED
@@ -32,7 +32,11 @@ export interface InstallServersRequestBody {
32
32
  }
33
33
 
34
34
  interface UninstallServersRequestBody {
35
- serverList: string[];
35
+ serverList: Record<string, { removeData?: boolean }>;
36
+ options: {
37
+ targets: string[];
38
+ removeData?: boolean;
39
+ };
36
40
  }
37
41
 
38
42
  interface ApiResponse<T> {
@@ -155,15 +159,29 @@ app.post('/api/categories/:categoryName/uninstall', async (req: Request<{ catego
155
159
  const { categoryName } = req.params;
156
160
  const { serverList } = req.body;
157
161
 
158
- if (!Array.isArray(serverList) || serverList.length === 0) {
162
+ if (!serverList || Object.keys(serverList).length === 0) {
159
163
  return res.status(400).json({
160
164
  success: false,
161
165
  error: 'Invalid tool list provided'
162
166
  });
163
167
  }
164
168
 
169
+ const { options } = req.body;
170
+ if (!options?.targets || options.targets.length === 0) {
171
+ return res.status(400).json({
172
+ success: false,
173
+ error: 'No target clients specified'
174
+ });
175
+ }
176
+
165
177
  const results = await Promise.all(
166
- serverList.map(serverName => serverService.uninstallMcpServer(categoryName, serverName))
178
+ Object.entries(serverList).map(([serverName, serverOptions]) =>
179
+ serverService.uninstallMcpServer(categoryName, serverName, {
180
+ ...serverOptions,
181
+ targets: options.targets,
182
+ removeData: options.removeData ?? serverOptions.removeData
183
+ })
184
+ )
167
185
  );
168
186
 
169
187
  const { success, messages } = serverService.formatOperationResults(results);