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.
@@ -3,19 +3,27 @@ import { serverService } from '../../services/ServerService.js';
3
3
  export function createUninstallCommand() {
4
4
  return new Command('uninstall')
5
5
  .description('Uninstall specific MCP servers')
6
- .requiredOption('--category <category>', '--names <names>', 'Server names (semicolon separated)')
7
- .option('--remove-data', 'Remove all associated data', false)
6
+ .requiredOption('--category <category>', 'Server category')
7
+ .requiredOption('--names <names>', 'Server names (semicolon separated)')
8
+ .requiredOption('--targets <targets>', 'Target clients to uninstall from (semicolon separated)')
9
+ .option('--remove-data', 'Remove all associated data', true // Change default to true to ensure cleanup happens
10
+ )
8
11
  .action(async (options) => {
9
12
  try {
10
13
  const serverNames = options.names.split(';').map((name) => name.trim());
11
- if (!serverService.validateServerName(options.category, serverNames)) {
14
+ const validNames = await serverService.validateServerName(options.category, serverNames);
15
+ if (!validNames) {
12
16
  console.error('Invalid server names provided');
13
17
  process.exit(1);
14
18
  }
15
19
  console.log(`Uninstalling servers: ${serverNames.join(', ')}`);
16
- const results = await Promise.all(serverNames.map((serverName) => serverService.uninstallMcpServer(options.category, serverName, {
17
- removeData: options.removeData,
18
- })));
20
+ const results = await Promise.all(serverNames.map((serverName) => {
21
+ const targets = options.targets ? options.targets.split(';').map((t) => t.trim()) : [];
22
+ return serverService.uninstallMcpServer(options.category, serverName, {
23
+ removeData: options.removeData,
24
+ targets: targets
25
+ });
26
+ }));
19
27
  const { success, messages } = serverService.formatOperationResults(results);
20
28
  messages.forEach(message => {
21
29
  if (success) {
@@ -28,5 +28,6 @@ export declare class ConfigurationProvider {
28
28
  private loadFeedsIntoConfiguration;
29
29
  private loadClientMCPSettings;
30
30
  reloadClientMCPSettings(): Promise<void>;
31
+ removeServerFromClientMCPSettings(serverName: string, target?: string): Promise<void>;
31
32
  }
32
33
  export declare const configProvider: ConfigurationProvider;
@@ -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 { ConfigurationLoader } from './ConfigurationLoader.js';
@@ -294,6 +294,70 @@ export class ConfigurationProvider {
294
294
  await this.loadClientMCPSettings();
295
295
  });
296
296
  }
297
+ async removeServerFromClientMCPSettings(serverName, target) {
298
+ return await this.withLock(async () => {
299
+ // Load utils in async context to avoid circular dependencies
300
+ const { readJsonFile, writeJsonFile } = await import('../utils/clientUtils.js');
301
+ // Filter clients if target is specified
302
+ const clientEntries = Object.entries(SUPPORTED_CLIENTS);
303
+ const targetClients = target
304
+ ? clientEntries.filter(([clientName]) => clientName === target)
305
+ : clientEntries;
306
+ for (const [clientName, clientSettings] of targetClients) {
307
+ const settingPath = process.env.CODE_INSIDERS
308
+ ? clientSettings.codeInsiderSettingPath
309
+ : clientSettings.codeSettingPath;
310
+ try {
311
+ const content = await readJsonFile(settingPath, true);
312
+ let modified = false;
313
+ // Handle GitHub Copilot's different structure
314
+ if (clientName === 'GithubCopilot' && content.mcp?.servers?.[serverName]) {
315
+ delete content.mcp.servers[serverName];
316
+ modified = true;
317
+ }
318
+ else if (content.mcpServers?.[serverName]) {
319
+ delete content.mcpServers[serverName];
320
+ modified = true;
321
+ }
322
+ // Only write if we actually modified the content
323
+ if (modified) {
324
+ await writeJsonFile(settingPath, content);
325
+ Logger.debug(`Removed server ${serverName} from client ${clientName} settings`);
326
+ }
327
+ }
328
+ catch (error) {
329
+ Logger.error(`Failed to remove server ${serverName} from client ${clientName} settings:`, error);
330
+ }
331
+ }
332
+ // Also update our in-memory configuration
333
+ if (this.configuration.clientMCPSettings) {
334
+ if (target) {
335
+ // Only update settings for the target client
336
+ const clientSettings = this.configuration.clientMCPSettings[target];
337
+ if (clientSettings) {
338
+ if (clientSettings.mcpServers?.[serverName]) {
339
+ delete clientSettings.mcpServers[serverName];
340
+ }
341
+ if (clientSettings.servers?.[serverName]) { // For GitHub Copilot
342
+ delete clientSettings.servers[serverName];
343
+ }
344
+ }
345
+ }
346
+ else {
347
+ // Update all clients if no target specified
348
+ for (const clientSettings of Object.values(this.configuration.clientMCPSettings)) {
349
+ if (clientSettings.mcpServers?.[serverName]) {
350
+ delete clientSettings.mcpServers[serverName];
351
+ }
352
+ if (clientSettings.servers?.[serverName]) { // For GitHub Copilot
353
+ delete clientSettings.servers[serverName];
354
+ }
355
+ }
356
+ }
357
+ await this.saveConfiguration();
358
+ }
359
+ });
360
+ }
297
361
  }
298
362
  // Export a singleton instance
299
363
  export const configProvider = ConfigurationProvider.getInstance();
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { ConfigurationProvider } from './ConfigurationProvider.js';
3
3
  import { InstallationService } from './InstallationService.js';
4
- import { MCPEvent, } from './types.js';
4
+ import { MCPEvent } from './types.js';
5
5
  export class MCPManager extends EventEmitter {
6
6
  installationService;
7
7
  configProvider;
@@ -66,12 +66,34 @@ export class MCPManager extends EventEmitter {
66
66
  message: `Server category ${categoryName} is not onboarded`,
67
67
  };
68
68
  }
69
- // Clear installation status
70
- await this.configProvider.updateInstallationStatus(categoryName, {}, {});
71
- this.emit(MCPEvent.SERVER_UNINSTALLED, { serverName });
69
+ const { targets = [], removeData = false } = options;
70
+ // Clear installation status for specified targets
71
+ const currentStatus = serverCategory.installationStatus || {
72
+ requirementsStatus: {},
73
+ serversStatus: {},
74
+ lastUpdated: new Date().toISOString()
75
+ };
76
+ const serversStatus = currentStatus.serversStatus || {};
77
+ const serverStatus = serversStatus[serverName] || { installedStatus: {}, name: serverName };
78
+ // Only reset installedStatus for specified targets
79
+ for (const target of targets) {
80
+ if (serverStatus.installedStatus) {
81
+ delete serverStatus.installedStatus[target];
82
+ }
83
+ }
84
+ if (removeData) {
85
+ for (const target of targets) {
86
+ await this.configProvider.removeServerFromClientMCPSettings(serverName, target);
87
+ }
88
+ }
89
+ // Update server status
90
+ serversStatus[serverName] = serverStatus;
91
+ // Update status keeping requirements
92
+ await this.configProvider.updateInstallationStatus(categoryName, currentStatus.requirementsStatus || {}, serversStatus);
93
+ this.emit(MCPEvent.SERVER_UNINSTALLED, { serverName, targets });
72
94
  return {
73
95
  success: true,
74
- message: `Successfully uninstalled ${serverName}`,
96
+ message: `Successfully uninstalled ${serverName} from ${targets.join(', ')}`,
75
97
  };
76
98
  }
77
99
  catch (error) {
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
- import { LOCAL_FEEDS_DIR } from './constants.js';
3
+ import { LOCAL_FEEDS_SCHEMA_DIR } from './constants.js';
4
4
  import { Logger } from '../utils/logger.js';
5
5
  export class ServerSchemaLoader {
6
6
  /**
@@ -8,7 +8,7 @@ export class ServerSchemaLoader {
8
8
  */
9
9
  static async loadSchema(categoryName, serverName) {
10
10
  try {
11
- const schemaPath = path.join(LOCAL_FEEDS_DIR, categoryName, `${serverName}.json`);
11
+ const schemaPath = path.join(LOCAL_FEEDS_SCHEMA_DIR, categoryName, `${serverName}.json`);
12
12
  const content = await fs.readFile(schemaPath, 'utf8');
13
13
  const schema = JSON.parse(content);
14
14
  // Validate schema structure
@@ -22,7 +22,17 @@ export declare const LOCAL_FEEDS_DIR: string;
22
22
  * Value: Client-specific settings or configuration details.
23
23
  * TODO: Define actual client settings structure.
24
24
  */
25
- export declare const SUPPORTED_CLIENTS: Record<string, any>;
25
+ export declare const SUPPORTED_CLIENTS: Record<string, {
26
+ extension: {
27
+ extensionId: string;
28
+ leastVersion?: string;
29
+ repository?: string;
30
+ assetName?: string;
31
+ private?: boolean;
32
+ };
33
+ codeSettingPath: string;
34
+ codeInsiderSettingPath: string;
35
+ }>;
26
36
  /**
27
37
  * List of supported client names.
28
38
  */
@@ -128,6 +128,9 @@ export class ExtensionInstaller {
128
128
  else {
129
129
  // Install private extension from GitHub release using latest version
130
130
  try {
131
+ if (!repository || !assetName) {
132
+ throw new Error(`Missing repository or assetName for private extension ${extensionId}`);
133
+ }
131
134
  const { resolvedPath } = await handleGitHubRelease({ name: extensionId, version: 'latest', type: 'extension' }, { repository, assetName });
132
135
  success = await this.installPrivateExtension(resolvedPath, isInsiders);
133
136
  }
@@ -73,6 +73,7 @@ export interface UpdateRequirementOptions {
73
73
  }
74
74
  export interface ServerUninstallOptions {
75
75
  removeData?: boolean;
76
+ targets?: string[];
76
77
  }
77
78
  export interface EnvVariableConfig {
78
79
  Required: boolean;
@@ -130,6 +131,10 @@ export interface FeedConfiguration {
130
131
  requirements: RequirementConfig[];
131
132
  mcpServers: McpConfig[];
132
133
  }
134
+ export interface ClientSettings {
135
+ codeSettingPath: string;
136
+ codeInsiderSettingPath: string;
137
+ }
133
138
  export declare enum MCPEvent {
134
139
  SERVER_INSTALLED = "server:installed",
135
140
  SERVER_UNINSTALLED = "server:uninstalled",
@@ -143,6 +148,7 @@ export interface MCPEventData {
143
148
  };
144
149
  [MCPEvent.SERVER_UNINSTALLED]: {
145
150
  serverName: string;
151
+ targets?: string[];
146
152
  };
147
153
  [MCPEvent.SERVER_STARTED]: {
148
154
  server: MCPServerCategory;
@@ -27,11 +27,17 @@ export declare class ServerService {
27
27
  * Installs a specific mcp tool for a server.
28
28
  * TODO: This might require enhancing MCPManager to handle category-specific installs.
29
29
  */
30
+ /**
31
+ * Uninstall MCP server from specified client targets
32
+ * @param category The server category
33
+ * @param serverName The server name to uninstall
34
+ * @param options Uninstall options including target clients and data removal flags
35
+ */
30
36
  uninstallMcpServer(category: string, serverName: string, options?: ServerUninstallOptions): Promise<ServerOperationResult>;
31
37
  /**
32
38
  * Validates server names
33
39
  */
34
- validateServerName(category: string, name: string): Promise<boolean>;
40
+ validateServerName(category: string, names: string | string[]): Promise<boolean>;
35
41
  /**
36
42
  * Formats success/error messages for operations
37
43
  */
@@ -117,25 +117,40 @@ export class ServerService {
117
117
  * Installs a specific mcp tool for a server.
118
118
  * TODO: This might require enhancing MCPManager to handle category-specific installs.
119
119
  */
120
- async uninstallMcpServer(category, serverName, options = {} // Reuse ServerInstallOptions for env etc.
121
- ) {
122
- return mcpManager.uninstallServer(category, serverName, options);
120
+ /**
121
+ * Uninstall MCP server from specified client targets
122
+ * @param category The server category
123
+ * @param serverName The server name to uninstall
124
+ * @param options Uninstall options including target clients and data removal flags
125
+ */
126
+ async uninstallMcpServer(category, serverName, options = {}) {
127
+ Logger.debug(`Uninstalling MCP server: ${JSON.stringify({ category, serverName, options })}`);
128
+ try {
129
+ const result = await mcpManager.uninstallServer(category, serverName, options);
130
+ Logger.debug(`Uninstallation result: ${JSON.stringify(result)}`);
131
+ return result;
132
+ }
133
+ catch (error) {
134
+ Logger.error(`Failed to uninstall MCP server: ${serverName}`, error);
135
+ throw error;
136
+ }
123
137
  }
124
138
  /**
125
139
  * Validates server names
126
140
  */
127
- async validateServerName(category, name) {
128
- Logger.debug(`Validating server name: ${JSON.stringify({ category, name })}`);
141
+ async validateServerName(category, names) {
142
+ const serverNames = Array.isArray(names) ? names : [names];
143
+ Logger.debug(`Validating server names: ${JSON.stringify({ category, names: serverNames })}`);
129
144
  // Check if category exists in feeds
130
145
  const feedConfig = await mcpManager.getFeedConfiguration(category);
131
146
  if (!feedConfig) {
132
147
  Logger.debug(`Validation failed: Category "${category}" not found in feeds`);
133
148
  return false;
134
149
  }
135
- // Check if server exists in the category's mcpServers
136
- const serverExists = feedConfig.mcpServers.some(server => server.name === name);
137
- if (!serverExists) {
138
- Logger.debug(`Validation failed: Server "${name}" not found in category "${category}"`);
150
+ // Check if all servers exist in the category's mcpServers
151
+ const invalidServers = serverNames.filter(name => !feedConfig.mcpServers.some(server => server.name === name));
152
+ if (invalidServers.length > 0) {
153
+ Logger.debug(`Validation failed: Servers "${invalidServers.join(', ')}" not found in category "${category}"`);
139
154
  return false;
140
155
  }
141
156
  return true;
@@ -256,7 +256,7 @@ export function getPythonPackagePath(pythonExecutablePath) {
256
256
  const venvRoot = path.dirname(dir);
257
257
  return path.join(venvRoot, 'Lib', 'site-packages');
258
258
  }
259
- else if (dir.toLowerCase().includes('python')) {
259
+ else {
260
260
  // System Python or Conda on Windows
261
261
  return path.join(dir, 'Lib', 'site-packages');
262
262
  }
@@ -1,56 +1,84 @@
1
1
  .details-widget {
2
- max-height: 0;
3
- overflow: hidden;
4
- transition: max-height 0.3s ease-out;
2
+ transition: all 0.3s ease-in-out;
3
+ }
4
+
5
+ .tools-grid {
6
+ display: grid;
7
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
8
+ gap: 0.5rem;
9
+ padding: 0.5rem;
5
10
  }
6
11
 
7
- .details-widget.expanded {
8
- max-height: 2000px;
9
- transition: max-height 0.5s ease-in;
12
+ .tool-card {
13
+ transition: all 0.3s ease-out;
14
+ border: 1px solid #e5e7eb;
15
+ margin-bottom: 0.5rem;
16
+ padding: 0.75rem;
10
17
  }
11
18
 
12
- .details-widget-content {
13
- padding: 1rem;
19
+ .tool-card.active {
20
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
21
+ border-left: 4px solid #3b82f6;
14
22
  background-color: #f8fafc;
15
- border-top: 1px solid #e2e8f0;
16
23
  }
17
24
 
18
- /* Schema specific styles */
19
- .schema-content {
20
- font-family: system-ui, -apple-system, sans-serif;
25
+ .tool-card-header {
26
+ position: relative;
27
+ padding-right: 2rem;
21
28
  }
22
29
 
23
- .tool-section {
24
- background-color: white;
25
- border-radius: 0.5rem;
26
- box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
27
- margin-bottom: 1.5rem;
28
- transition: transform 0.2s;
30
+ .tool-card-header::after {
31
+ content: '';
32
+ position: absolute;
33
+ right: 1rem;
34
+ top: 50%;
35
+ transform: translateY(-50%);
36
+ width: 12px;
37
+ height: 12px;
38
+ border-right: 2px solid #64748b;
39
+ border-bottom: 2px solid #64748b;
40
+ transform-origin: 75% 75%;
41
+ transition: transform 0.3s ease;
42
+ }
43
+
44
+ .tool-card.active .tool-card-header::after {
45
+ transform: translateY(-50%) rotate(45deg);
46
+ }
47
+
48
+ .tool-details {
49
+ max-height: 0;
50
+ opacity: 0;
51
+ overflow: hidden;
52
+ transition: all 0.3s ease-out;
29
53
  }
30
54
 
31
- .tool-section:hover {
32
- transform: translateY(-2px);
55
+ .tool-details.visible {
56
+ max-height: 1500px;
57
+ opacity: 1;
58
+ padding-top: 0.75rem;
59
+ margin-top: 0.75rem;
60
+ border-top: 1px solid #e5e7eb;
33
61
  }
34
62
 
35
63
  .property-item {
36
- padding: 0.75rem;
37
- margin: 0.5rem 0;
38
- background-color: white;
39
- border-radius: 0.375rem;
40
- transition: all 0.2s;
64
+ margin-bottom: 0.75rem;
65
+ padding-left: 0.75rem;
66
+ border-left: 2px solid #e5e7eb;
67
+ transition: border-color 0.2s ease;
68
+ font-size: 0.9rem;
41
69
  }
42
70
 
43
71
  .property-item:hover {
44
- background-color: #f1f5f9;
72
+ border-left-color: #3b82f6;
45
73
  }
46
74
 
47
- .property-item.required {
48
- border-left: 3px solid #3b82f6;
75
+ .property-header {
76
+ margin-bottom: 0.5rem;
49
77
  }
50
78
 
51
79
  .property-name {
52
- color: #1e40af;
53
80
  font-weight: 600;
81
+ color: #1e293b;
54
82
  }
55
83
 
56
84
  .property-type {
@@ -60,44 +88,23 @@
60
88
 
61
89
  .property-desc {
62
90
  color: #475569;
63
- margin-top: 0.375rem;
91
+ font-size: 0.8125rem;
64
92
  line-height: 1.4;
65
- }
66
-
67
- .property-default {
68
- color: #94a3b8;
69
- font-size: 0.875rem;
70
- font-family: monospace;
71
- }
72
-
73
- .required-badge {
74
- background-color: #dbeafe;
75
- color: #1e40af;
76
- padding: 0.125rem 0.5rem;
77
- border-radius: 9999px;
78
- font-size: 0.75rem;
79
- font-weight: 500;
93
+ margin-top: 0.25rem;
80
94
  }
81
95
 
82
96
  .required-fields {
83
97
  background-color: #fef3c7;
84
- color: #92400e;
85
- padding: 0.5rem;
86
- border-radius: 0.375rem;
98
+ border-left: 4px solid #f59e0b;
99
+ padding: 0.5rem 0.75rem;
87
100
  margin-bottom: 1rem;
88
101
  font-size: 0.875rem;
89
102
  }
90
103
 
91
- .input-schema {
92
- margin-top: 1rem;
93
- padding: 1rem;
94
- background-color: #f8fafc;
95
- border-radius: 0.5rem;
96
- border: 1px solid #e2e8f0;
97
- }
98
-
99
- .properties-list {
100
- display: flex;
101
- flex-direction: column;
102
- gap: 0.75rem;
104
+ .nested-properties {
105
+ margin-left: 0.75rem;
106
+ padding-left: 0.75rem;
107
+ border-left: 1px solid #e5e7eb;
108
+ margin-top: 0.5rem;
109
+ font-size: 0.875rem;
103
110
  }
@@ -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%;
@@ -36,8 +36,9 @@
36
36
  }
37
37
 
38
38
  .details-widget.expanded {
39
- max-height: 500px; /* Adjust based on content */
39
+ max-height: 800px; /* Increased height to accommodate more content */
40
40
  border-color: #3b82f6;
41
+ transition: max-height 0.3s ease-in-out;
41
42
  }
42
43
 
43
44
  .details-widget-content {