imcp 0.0.11 → 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.
@@ -2,13 +2,20 @@ export class DetailsWidget {
2
2
  constructor(container) {
3
3
  this.container = container;
4
4
  this.isExpanded = false;
5
+ this.expandedTool = null;
5
6
  this.init();
6
7
  }
7
8
 
8
9
  init() {
9
- // Create details widget elements
10
+ // Create and append stylesheet
11
+ const link = document.createElement('link');
12
+ link.rel = 'stylesheet';
13
+ link.href = '/css/detailsWidget.css';
14
+ document.head.appendChild(link);
15
+
10
16
  this.widgetElement = document.createElement('div');
11
17
  this.widgetElement.className = 'details-widget';
18
+ this.widgetElement.style.overflow = 'auto'; // Enable scrolling
12
19
  this.contentElement = document.createElement('div');
13
20
  this.contentElement.className = 'details-widget-content';
14
21
  this.widgetElement.appendChild(this.contentElement);
@@ -25,41 +32,130 @@ export class DetailsWidget {
25
32
  return;
26
33
  }
27
34
 
28
- // If content is a schema object
29
- if (content.schema) {
30
- this.contentElement.innerHTML = this.renderSchema(content.schema);
35
+ // Handle direct schema object or wrapped schema object
36
+ if (typeof content === 'object') {
37
+ const schemaData = content.schema || content; // Handle both {schema: data} and direct data
38
+ this.renderToolsList(schemaData);
39
+ return;
31
40
  }
41
+
42
+ this.contentElement.innerHTML = '<p>Invalid content format</p>';
32
43
  }
33
44
 
34
- renderSchema(schema) {
35
- if (!schema || typeof schema !== 'object') {
36
- return '<p>Invalid schema format</p>';
45
+ // Maintain compatibility with old toggle behavior
46
+ toggle() {
47
+ if (this.isExpanded) {
48
+ this.collapse();
49
+ } else {
50
+ this.expand();
37
51
  }
52
+ }
38
53
 
39
- // The schema object from the API is wrapped in a "schema" property
40
- const schemaContent = schema.schema || schema;
54
+ expand() {
55
+ this.isExpanded = true;
56
+ this.widgetElement.classList.add('expanded');
57
+ if (this.container.querySelector('.server-item-content')) {
58
+ this.container.querySelector('.server-item-content').classList.add('expanded');
59
+ }
60
+ }
41
61
 
42
- let html = '<div class="schema-content p-4">';
62
+ collapse() {
63
+ this.isExpanded = false;
64
+ this.widgetElement.classList.remove('expanded');
65
+ if (this.container.querySelector('.server-item-content')) {
66
+ this.container.querySelector('.server-item-content').classList.remove('expanded');
67
+ }
68
+ // Reset expanded tool state
69
+ this.expandedTool = null;
70
+ }
71
+
72
+ renderToolsList(schemaContent) {
73
+ if (!schemaContent || typeof schemaContent !== 'object') {
74
+ this.contentElement.innerHTML = '<p>Invalid schema format</p>';
75
+ return;
76
+ }
77
+
78
+ let html = `
79
+ <div class="tools-list p-2">
80
+ <div class="tools-grid grid gap-2">
81
+ `;
43
82
 
44
- // Iterate through each tool in schema
45
83
  Object.entries(schemaContent).forEach(([toolName, toolInfo]) => {
46
- if (!toolInfo) return; // Skip if tool info is undefined
84
+ if (!toolInfo) return;
47
85
 
48
86
  html += `
49
- <div class="tool-section mb-6 border-b border-gray-200 pb-4">
50
- <h3 class="text-lg font-semibold text-blue-600 mb-2">${toolInfo.name || toolName}</h3>
51
- <p class="text-gray-700 mb-4">${toolInfo.description || 'No description available'}</p>
52
-
53
- <div class="input-schema bg-gray-50 p-4 rounded-lg">
54
- <h4 class="text-md font-semibold text-gray-700 mb-2">Input Schema</h4>
87
+ <div class="tool-card" data-tool="${this.escapeHtml(toolName)}">
88
+ <div class="tool-card-header">
89
+ <div class="tool-header mb-2">
90
+ <h3 class="text-md font-semibold text-blue-600">${toolInfo.name || toolName}</h3>
91
+ </div>
92
+ <p class="text-gray-700 text-sm">${toolInfo.description || 'No description available'}</p>
93
+ </div>
94
+ <div class="tool-details hidden mt-3" data-tool-details="${this.escapeHtml(toolName)}">
55
95
  ${this.renderInputSchema(toolInfo.inputSchema)}
56
96
  </div>
57
97
  </div>
58
98
  `;
59
99
  });
60
100
 
61
- html += '</div>';
62
- return html;
101
+ html += `
102
+ </div>
103
+ </div>
104
+ `;
105
+
106
+ this.contentElement.innerHTML = html;
107
+
108
+ // Add event listeners to tool cards
109
+ this.contentElement.querySelectorAll('.tool-card').forEach(card => {
110
+ const header = card.querySelector('.tool-card-header');
111
+ header.addEventListener('click', (e) => {
112
+ e.stopPropagation(); // Prevent click from bubbling up to server-item
113
+ const toolName = card.dataset.tool;
114
+ this.toggleToolDetails(toolName, card);
115
+ });
116
+
117
+ // Add click handler for the details area to prevent bubbling
118
+ const details = card.querySelector('.tool-details');
119
+ details.addEventListener('click', (e) => {
120
+ e.stopPropagation(); // Prevent click from bubbling up
121
+ });
122
+ });
123
+ }
124
+
125
+ toggleToolDetails(toolName, card) {
126
+ const detailsElement = card.querySelector(`[data-tool-details="${toolName}"]`);
127
+ const isExpanded = !detailsElement.classList.contains('hidden');
128
+
129
+ // Collapse all other expanded tools
130
+ if (this.expandedTool && this.expandedTool !== toolName) {
131
+ const prevCard = this.contentElement.querySelector(`[data-tool="${this.expandedTool}"]`);
132
+ if (prevCard) {
133
+ const prevDetails = prevCard.querySelector(`[data-tool-details="${this.expandedTool}"]`);
134
+ prevDetails?.classList.add('hidden');
135
+ prevCard.classList.remove('active');
136
+ setTimeout(() => {
137
+ prevDetails.classList.remove('visible');
138
+ }, 0);
139
+ }
140
+ }
141
+
142
+ // Toggle current tool with animation
143
+ if (isExpanded) {
144
+ detailsElement.classList.remove('visible');
145
+ card.classList.remove('active');
146
+ setTimeout(() => {
147
+ detailsElement.classList.add('hidden');
148
+ }, 300); // Match transition duration from CSS
149
+ this.expandedTool = null;
150
+ } else {
151
+ detailsElement.classList.remove('hidden');
152
+ card.classList.add('active');
153
+ setTimeout(() => {
154
+ detailsElement.classList.add('visible');
155
+ }, 0);
156
+ this.expandedTool = toolName;
157
+ detailsElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
158
+ }
63
159
  }
64
160
 
65
161
  renderInputSchema(schema) {
@@ -67,7 +163,6 @@ export class DetailsWidget {
67
163
  return '<p>No input schema available</p>';
68
164
  }
69
165
 
70
- // Handle case where schema might be empty or have no properties
71
166
  if (!schema.properties || Object.keys(schema.properties).length === 0) {
72
167
  return '<p>No input properties defined</p>';
73
168
  }
@@ -75,55 +170,77 @@ export class DetailsWidget {
75
170
  let html = '<div class="properties-list">';
76
171
 
77
172
  try {
78
- // Required fields banner if any
79
- if (schema.required && Array.isArray(schema.required) && schema.required.length > 0) {
173
+ // Required fields banner
174
+ if (schema.required?.length > 0) {
80
175
  html += `
81
176
  <div class="required-fields mb-3 bg-yellow-50 p-2 rounded">
82
- <span class="text-sm font-medium text-yellow-800">Required fields: ${schema.required.join(', ')}</span>
177
+ <span class="text-sm font-medium text-yellow-800">
178
+ Required: ${schema.required.join(', ')}
179
+ </span>
83
180
  </div>
84
181
  `;
85
182
  }
86
183
 
87
184
  // Render each property
88
185
  Object.entries(schema.properties).forEach(([propName, propDetails]) => {
89
- if (!propDetails) return; // Skip if property details are undefined
186
+ if (!propDetails) return;
90
187
 
91
- const isRequired = schema.required && Array.isArray(schema.required) && schema.required.includes(propName);
92
- const type = propDetails.type || 'any';
188
+ const isRequired = schema.required?.includes(propName);
189
+ const type = this.getPropertyType(propDetails);
93
190
 
94
191
  html += `
95
- <div class="property-item mb-3 ${isRequired ? 'required' : ''} border-l-2 ${isRequired ? 'border-blue-500' : 'border-gray-300'} pl-3">
96
- <div class="property-header flex items-center gap-2">
97
- <span class="property-name font-medium text-gray-900">${this.escapeHtml(propName)}</span>
98
- <span class="property-type text-sm text-gray-500">(${this.escapeHtml(type)})</span>
99
- ${isRequired ? '<span class="required-badge text-xs bg-blue-100 text-blue-800 px-2 py-0.5 rounded">Required</span>' : ''}
192
+ <div class="property-item ${isRequired ? 'required' : ''}">
193
+ <div class="property-header">
194
+ <span class="property-name">${this.escapeHtml(propName)}</span>
195
+ <span class="property-type">${this.escapeHtml(type)}</span>
196
+ ${isRequired ? '<span class="required-badge">Required</span>' : ''}
100
197
  </div>
101
- ${propDetails.description ? `<p class="property-desc text-sm text-gray-600 mt-1">${this.escapeHtml(propDetails.description)}</p>` : ''}
102
- ${propDetails.default !== undefined ? `<p class="property-default text-sm text-gray-500 mt-1">Default: ${this.escapeHtml(JSON.stringify(propDetails.default))}</p>` : ''}
198
+ ${propDetails.description ?
199
+ `<p class="property-desc">${this.escapeHtml(propDetails.description)}</p>` : ''}
200
+ ${propDetails.default !== undefined ?
201
+ `<p class="property-default">Default: ${this.escapeHtml(JSON.stringify(propDetails.default))}</p>` : ''}
202
+ ${this.renderNestedProperties(propDetails)}
103
203
  </div>
104
204
  `;
105
205
  });
106
206
 
107
- html += '</div>';
108
- return html;
207
+ return html + '</div>';
109
208
  } catch (error) {
110
209
  console.error('Error rendering input schema:', error);
111
210
  return '<p>Error rendering input schema</p>';
112
211
  }
113
212
  }
114
213
 
115
- toggle() {
116
- this.isExpanded = !this.isExpanded;
117
- if (this.isExpanded) {
118
- this.expand();
119
- } else {
120
- this.collapse();
214
+ renderNestedProperties(propDetails) {
215
+ if (propDetails.properties) {
216
+ let html = '<div class="nested-properties">';
217
+ Object.entries(propDetails.properties).forEach(([name, details]) => {
218
+ const type = this.getPropertyType(details);
219
+ html += `
220
+ <div class="nested-property-item">
221
+ <div class="property-header">
222
+ <span class="property-name">${this.escapeHtml(name)}</span>
223
+ <span class="property-type">${this.escapeHtml(type)}</span>
224
+ </div>
225
+ ${details.description ?
226
+ `<p class="property-desc">${this.escapeHtml(details.description)}</p>` : ''}
227
+ </div>
228
+ `;
229
+ });
230
+ return html + '</div>';
121
231
  }
232
+ return '';
122
233
  }
123
234
 
124
- expand() {
125
- this.widgetElement.classList.add('expanded');
126
- this.container.querySelector('.server-item-content').classList.add('expanded');
235
+ getPropertyType(propDetails) {
236
+ if (propDetails.anyOf) {
237
+ return `oneOf: [${propDetails.anyOf.map(type => type.type || 'any').join(', ')}]`;
238
+ }
239
+ if (propDetails.type === 'array') {
240
+ const itemType = propDetails.items?.type || 'any';
241
+ return `array<${itemType}>`;
242
+ }
243
+ return propDetails.type || 'any';
127
244
  }
128
245
 
129
246
  escapeHtml(unsafe) {
@@ -138,11 +255,6 @@ export class DetailsWidget {
138
255
  .replace(/'/g, "&#039;");
139
256
  }
140
257
 
141
- collapse() {
142
- this.widgetElement.classList.remove('expanded');
143
- this.container.querySelector('.server-item-content').classList.remove('expanded');
144
- }
145
-
146
258
  isVisible() {
147
259
  return this.isExpanded;
148
260
  }
@@ -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.11",
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",