imcp 0.0.1 → 0.0.2

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 (38) hide show
  1. package/dist/cli/commands/install.js +8 -8
  2. package/dist/cli/index.js +3 -2
  3. package/dist/core/ConfigurationProvider.d.ts +2 -0
  4. package/dist/core/ConfigurationProvider.js +49 -3
  5. package/dist/core/InstallationService.d.ts +8 -0
  6. package/dist/core/InstallationService.js +117 -0
  7. package/dist/core/MCPManager.d.ts +1 -0
  8. package/dist/core/MCPManager.js +42 -0
  9. package/dist/core/RequirementService.d.ts +7 -0
  10. package/dist/core/RequirementService.js +17 -0
  11. package/dist/core/constants.d.ts +5 -0
  12. package/dist/core/constants.js +9 -4
  13. package/dist/core/installers/BaseInstaller.js +26 -9
  14. package/dist/core/installers/GeneralInstaller.js +0 -5
  15. package/dist/core/installers/NpmInstaller.js +2 -1
  16. package/dist/core/types.d.ts +7 -6
  17. package/dist/services/ServerService.js +16 -0
  18. package/dist/utils/versionUtils.d.ts +12 -0
  19. package/dist/utils/versionUtils.js +26 -0
  20. package/dist/web/public/js/modal.js +231 -46
  21. package/dist/web/server.d.ts +6 -0
  22. package/dist/web/server.js +6 -1
  23. package/package.json +1 -1
  24. package/src/cli/commands/install.ts +11 -14
  25. package/src/cli/index.ts +4 -2
  26. package/src/core/ConfigurationProvider.ts +51 -3
  27. package/src/core/InstallationService.ts +131 -0
  28. package/src/core/MCPManager.ts +60 -1
  29. package/src/core/RequirementService.ts +21 -1
  30. package/src/core/constants.ts +11 -5
  31. package/src/core/installers/BaseInstaller.ts +33 -17
  32. package/src/core/installers/GeneralInstaller.ts +0 -5
  33. package/src/core/installers/NpmInstaller.ts +2 -1
  34. package/src/core/types.ts +8 -6
  35. package/src/services/ServerService.ts +22 -0
  36. package/src/utils/versionUtils.ts +29 -0
  37. package/src/web/public/js/modal.js +231 -46
  38. package/src/web/server.ts +16 -2
@@ -19,7 +19,6 @@ Examples:
19
19
  `)
20
20
  .requiredOption('--category <category>', 'Server category')
21
21
  .requiredOption('--name <name>', 'Server name to install')
22
- .option('--force', 'Force installation even if server already exists', false)
23
22
  .option('--clients <clients>', 'Target clients (semicolon separated). Supported values: Cline, MSRooCode, GithubCopilot. If not specified, installs for all clients')
24
23
  .option('--envs <envs>', 'Environment variables (semicolon separated key=value pairs)')
25
24
  .action(async (options) => {
@@ -30,15 +29,16 @@ Examples:
30
29
  Logger.log('Local feeds not found, syncing from remote...');
31
30
  await serverService.syncFeeds();
32
31
  }
33
- const { category, name, verbose, force, clients, envs } = options;
34
- Logger.debug(`Install options: ${JSON.stringify({ category, name, force, verbose, clients, envs })}`);
32
+ const { category, name, verbose, clients, envs } = options;
33
+ Logger.debug(`Install options: ${JSON.stringify({ category, name, verbose, clients, envs })}`);
35
34
  const serverName = name.trim();
36
35
  Logger.debug(`Server name: ${serverName}`);
37
36
  if (!await serverService.validateServerName(category, serverName)) {
38
- Logger.error('Invalid server name or category provided', {
39
- category,
40
- serverName
41
- });
37
+ Logger.error('Invalid server name or category provided.\n' +
38
+ 'This could be because:\n' +
39
+ ' 1. The server name or category is misspelled\n' +
40
+ ' 2. Your local feeds are outdated\n\n' +
41
+ 'Try running "imcp pull" to update your local feeds from remote.', { category, serverName });
42
42
  process.exit(1);
43
43
  }
44
44
  // Parse and validate clients
@@ -77,7 +77,7 @@ Examples:
77
77
  }
78
78
  Logger.log(`Installing server: ${serverName}`);
79
79
  const installOptions = {
80
- force: options.force,
80
+ force: false,
81
81
  ...(parsedClients?.length && { targetClients: parsedClients }),
82
82
  ...(Object.keys(parsedEnvs).length > 0 && { env: parsedEnvs })
83
83
  };
package/dist/cli/index.js CHANGED
@@ -10,6 +10,7 @@ import axios from 'axios';
10
10
  import path from 'path';
11
11
  import { fileURLToPath } from 'url';
12
12
  import fs from 'fs';
13
+ import { compareVersions } from '../utils/versionUtils.js';
13
14
  // Custom error interface for Commander.js errors
14
15
  // ANSI color codes
15
16
  const COLORS = {
@@ -23,7 +24,6 @@ async function main() {
23
24
  program
24
25
  .name('imcp')
25
26
  .description('IMCP (Install Model Context Protocol) CLI')
26
- .version('0.0.1')
27
27
  .option('--verbose', 'Show detailed logs for all commands');
28
28
  // Parse global options first
29
29
  program.parseOptions(process.argv);
@@ -85,7 +85,8 @@ async function checkForUpdates() {
85
85
  const npmResponse = await axios.get(`https://registry.npmjs.org/${packageName}`);
86
86
  if (npmResponse.data && npmResponse.data['dist-tags'] && npmResponse.data['dist-tags'].latest) {
87
87
  const latestVersion = npmResponse.data['dist-tags'].latest;
88
- if (latestVersion && latestVersion !== currentVersion) {
88
+ // Compare versions properly to ensure we're only notifying for newer versions
89
+ if (latestVersion && compareVersions(latestVersion, currentVersion) > 0) {
89
90
  console.log(`${COLORS.yellow}Update available for ${packageName}: ${currentVersion} → ${latestVersion}${COLORS.reset}`);
90
91
  console.log(`${COLORS.yellow}Run \`npm install -g ${packageName}@latest\` to update${COLORS.reset}`);
91
92
  }
@@ -9,9 +9,11 @@ export declare class ConfigurationProvider {
9
9
  static getInstance(): ConfigurationProvider;
10
10
  private withLock;
11
11
  initialize(): Promise<void>;
12
+ private loadClientMCPSettings;
12
13
  private saveConfiguration;
13
14
  getServerCategories(): Promise<MCPServerCategory[]>;
14
15
  getServerCategory(categoryName: string): Promise<MCPServerCategory | undefined>;
16
+ getClientMcpSettings(): Promise<Record<string, Record<string, any>> | undefined>;
15
17
  getFeedConfiguration(categoryName: string): Promise<FeedConfiguration | undefined>;
16
18
  getInstallationStatus(categoryName: string): Promise<InstallationStatus | undefined>;
17
19
  getServerStatus(categoryName: string, serverName: string): Promise<MCPServerStatus | undefined>;
@@ -3,9 +3,10 @@ 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
+ import { readJsonFile } from '../utils/clientUtils.js';
9
10
  const execAsync = promisify(exec);
10
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
12
  export class ConfigurationProvider {
@@ -20,6 +21,7 @@ export class ConfigurationProvider {
20
21
  this.configuration = {
21
22
  localServerCategories: [],
22
23
  feeds: {},
24
+ clientMCPSettings: {}
23
25
  };
24
26
  this.tempDir = path.join(LOCAL_FEEDS_DIR, '../temp');
25
27
  }
@@ -49,6 +51,7 @@ export class ConfigurationProvider {
49
51
  const config = JSON.parse(await fs.readFile(this.configPath, 'utf8'));
50
52
  this.configuration = config;
51
53
  await this.loadFeedsIntoConfiguration(); // Load feeds into configuration
54
+ await this.loadClientMCPSettings(); // Load MCP settings for each client
52
55
  }
53
56
  catch (error) {
54
57
  if (error.code !== 'ENOENT') {
@@ -59,6 +62,45 @@ export class ConfigurationProvider {
59
62
  }
60
63
  });
61
64
  }
65
+ async loadClientMCPSettings() {
66
+ try {
67
+ Logger.debug('Starting to load MCP client settings...');
68
+ const settings = {};
69
+ for (const [clientName, clientSettings] of Object.entries(SUPPORTED_CLIENTS)) {
70
+ const settingPath = process.env.CODE_INSIDERS
71
+ ? clientSettings.codeInsiderSettingPath
72
+ : clientSettings.codeSettingPath;
73
+ try {
74
+ let content = await readJsonFile(settingPath, true);
75
+ if (clientName === 'GithubCopilot') {
76
+ // Initialize the mcp section if it doesn't exist
77
+ if (!content.mcp) {
78
+ content = {
79
+ servers: {},
80
+ inputs: []
81
+ };
82
+ }
83
+ else {
84
+ content = content.mcp;
85
+ }
86
+ }
87
+ settings[clientName] = content;
88
+ Logger.debug(`Successfully loaded MCP settings for ${clientName}`);
89
+ }
90
+ catch (error) {
91
+ Logger.debug(`Warning: Could not load MCP settings for client ${clientName}: ${error instanceof Error ? error.message : String(error)}`);
92
+ settings[clientName] = {};
93
+ }
94
+ }
95
+ this.configuration.clientMCPSettings = settings;
96
+ await this.saveConfiguration();
97
+ Logger.debug('Saved MCP client settings to configuration');
98
+ }
99
+ catch (error) {
100
+ Logger.error('Error loading client MCP settings:', error);
101
+ throw error;
102
+ }
103
+ }
62
104
  async saveConfiguration() {
63
105
  const configDir = path.dirname(this.configPath);
64
106
  await fs.mkdir(configDir, { recursive: true });
@@ -74,6 +116,11 @@ export class ConfigurationProvider {
74
116
  return this.configuration.localServerCategories.find(s => s.name === categoryName);
75
117
  });
76
118
  }
119
+ async getClientMcpSettings() {
120
+ return await this.withLock(async () => {
121
+ return this.configuration.clientMCPSettings;
122
+ });
123
+ }
77
124
  async getFeedConfiguration(categoryName) {
78
125
  return await this.withLock(async () => {
79
126
  return this.configuration.feeds[categoryName];
@@ -325,8 +372,7 @@ export class ConfigurationProvider {
325
372
  type: req.type,
326
373
  installed: false,
327
374
  version: req.version,
328
- error: undefined,
329
- updateInfo: null
375
+ error: undefined
330
376
  };
331
377
  }
332
378
  }
@@ -14,4 +14,12 @@ export declare class InstallationService {
14
14
  * @returns A result object indicating success or failure.
15
15
  */
16
16
  install(categoryName: string, serverName: string, options: ServerInstallOptions): Promise<ServerOperationResult>;
17
+ /**
18
+ * Process requirement updates specified in serverInstallOptions
19
+ * All updates are processed in parallel for maximum efficiency
20
+ * @param categoryName The category name
21
+ * @param serverName The server name
22
+ * @param requirements The requirements to update
23
+ */
24
+ private processRequirementUpdates;
17
25
  }
@@ -29,6 +29,15 @@ export class InstallationService {
29
29
  async install(categoryName, serverName, options) {
30
30
  const configProvider = ConfigurationProvider.getInstance();
31
31
  const clients = options.targetClients || Object.keys(SUPPORTED_CLIENTS);
32
+ // Process updates for requirements if specified in options
33
+ // Fire off requirement updates in the background without awaiting completion
34
+ if (options.requirements && options.requirements.length > 0) {
35
+ // Start the process but don't await it - it will run in the background
36
+ this.processRequirementUpdates(categoryName, serverName, options.requirements)
37
+ .catch(error => {
38
+ console.error(`Error in background requirement updates: ${error instanceof Error ? error.message : String(error)}`);
39
+ });
40
+ }
32
41
  // Check if server is already ready
33
42
  const isReady = await configProvider.isServerReady(categoryName, serverName, clients);
34
43
  if (isReady) {
@@ -138,6 +147,114 @@ export class InstallationService {
138
147
  // Each client installer will check requirements before actual installation
139
148
  return await clientInstaller.install(options);
140
149
  }
150
+ /**
151
+ * Process requirement updates specified in serverInstallOptions
152
+ * All updates are processed in parallel for maximum efficiency
153
+ * @param categoryName The category name
154
+ * @param serverName The server name
155
+ * @param requirements The requirements to update
156
+ */
157
+ async processRequirementUpdates(categoryName, serverName, requirements) {
158
+ // Use UpdateCheckTracker to prevent concurrent updates
159
+ const updateCheckTracker = await import('../utils/UpdateCheckTracker.js').then(m => m.updateCheckTracker);
160
+ const operationKey = `requirement-updates-${categoryName}-${serverName}`;
161
+ // Check if there's already an update operation in progress for this server
162
+ const canProceed = await updateCheckTracker.startOperation(operationKey);
163
+ if (!canProceed) {
164
+ console.log(`Requirement updates for ${categoryName}/${serverName} already in progress, skipping`);
165
+ return;
166
+ }
167
+ try {
168
+ const configProvider = ConfigurationProvider.getInstance();
169
+ const feedConfig = await configProvider.getFeedConfiguration(categoryName);
170
+ if (!feedConfig) {
171
+ console.error(`Feed configuration not found for category: ${categoryName}`);
172
+ return;
173
+ }
174
+ // Import the RequirementService
175
+ const { requirementService } = await import('./RequirementService.js');
176
+ // Create an array of promises to update all requirements in parallel
177
+ const updatePromises = requirements.map(async (reqToUpdate) => {
178
+ try {
179
+ // Find the full requirement config
180
+ const reqConfig = feedConfig.requirements?.find((r) => r.name === reqToUpdate.name);
181
+ if (!reqConfig) {
182
+ console.error(`Requirement configuration not found for: ${reqToUpdate.name}`);
183
+ return;
184
+ }
185
+ // Get current status
186
+ const currentStatus = await configProvider.getRequirementStatus(categoryName, reqToUpdate.name);
187
+ if (!currentStatus) {
188
+ console.error(`No current status found for requirement: ${reqToUpdate.name}`);
189
+ return;
190
+ }
191
+ // Update requirement status to indicate update in progress
192
+ await configProvider.updateRequirementStatus(categoryName, reqToUpdate.name, {
193
+ ...currentStatus,
194
+ name: reqToUpdate.name, // Ensure name is included
195
+ type: currentStatus.type || 'unknown', // Ensure type is included
196
+ installed: currentStatus.installed || false, // Ensure installed is included
197
+ inProgress: true,
198
+ operationStatus: {
199
+ status: 'in-progress',
200
+ type: 'update',
201
+ target: 'requirement',
202
+ message: `Updating ${reqToUpdate.name} from ${currentStatus.version || 'unknown'} to ${reqToUpdate.version}`
203
+ }
204
+ });
205
+ // Create updated requirement config with new version
206
+ const updatedReqConfig = {
207
+ ...reqConfig,
208
+ version: reqToUpdate.version
209
+ };
210
+ // Update the requirement
211
+ const updatedStatus = await requirementService.updateRequirement(updatedReqConfig, reqToUpdate.version);
212
+ // Update requirement status
213
+ await configProvider.updateRequirementStatus(categoryName, reqToUpdate.name, {
214
+ ...updatedStatus,
215
+ name: reqToUpdate.name, // Ensure name is always included
216
+ type: updatedStatus.type || currentStatus.type || 'unknown', // Ensure type is included
217
+ installed: updatedStatus.installed, // This should be defined from updatedStatus
218
+ inProgress: false,
219
+ operationStatus: {
220
+ status: updatedStatus.installed ? 'completed' : 'failed',
221
+ type: 'update',
222
+ target: 'requirement',
223
+ message: updatedStatus.installed
224
+ ? `Successfully updated ${reqToUpdate.name} to version ${reqToUpdate.version}`
225
+ : `Failed to update ${reqToUpdate.name} to version ${reqToUpdate.version}`
226
+ },
227
+ // Clear availableUpdate if update was successful
228
+ availableUpdate: updatedStatus.installed ? undefined : currentStatus.availableUpdate
229
+ });
230
+ console.log(`Requirement ${reqToUpdate.name} updated to version ${reqToUpdate.version}`);
231
+ }
232
+ catch (error) {
233
+ console.error(`Error updating requirement ${reqToUpdate.name}:`, error);
234
+ // Update status to indicate failure
235
+ await configProvider.updateRequirementStatus(categoryName, reqToUpdate.name, {
236
+ name: reqToUpdate.name,
237
+ type: 'unknown',
238
+ installed: false,
239
+ inProgress: false,
240
+ error: error instanceof Error ? error.message : String(error),
241
+ operationStatus: {
242
+ status: 'failed',
243
+ type: 'update',
244
+ target: 'requirement',
245
+ message: `Error updating requirement: ${error instanceof Error ? error.message : String(error)}`
246
+ }
247
+ });
248
+ }
249
+ });
250
+ // Wait for all updates to complete in parallel
251
+ await Promise.all(updatePromises);
252
+ }
253
+ finally {
254
+ // Always release the lock when done, even if there was an error
255
+ await updateCheckTracker.endOperation(operationKey);
256
+ }
257
+ }
141
258
  }
142
259
  // Export a singleton instance (optional)
143
260
  // export const installationService = new InstallationService();
@@ -10,6 +10,7 @@ export declare class MCPManager extends EventEmitter {
10
10
  getFeedConfiguration(categoryName: string): Promise<import("./types.js").FeedConfiguration | undefined>;
11
11
  installServer(categoryName: string, serverName: string, requestOptions?: ServerInstallOptions): Promise<ServerOperationResult>;
12
12
  uninstallServer(categoryName: string, serverName: string, options?: ServerUninstallOptions): Promise<ServerOperationResult>;
13
+ updateRequirement(categoryName: string, serverName: string, requirementName: string, updateVersion: string): Promise<ServerOperationResult>;
13
14
  emit<E extends MCPEvent>(event: E, data: MCPEventData[E]): boolean;
14
15
  on<E extends MCPEvent>(event: E, listener: (data: MCPEventData[E]) => void): this;
15
16
  off<E extends MCPEvent>(event: E, listener: (data: MCPEventData[E]) => void): this;
@@ -82,6 +82,48 @@ export class MCPManager extends EventEmitter {
82
82
  };
83
83
  }
84
84
  }
85
+ async updateRequirement(categoryName, serverName, requirementName, updateVersion) {
86
+ try {
87
+ const { requirementService } = await import('./RequirementService.js');
88
+ const serverCategory = await this.configProvider.getServerCategory(categoryName);
89
+ if (!serverCategory) {
90
+ return {
91
+ success: false,
92
+ message: `Server category ${categoryName} is not onboarded`,
93
+ };
94
+ }
95
+ if (!serverCategory.feedConfiguration) {
96
+ return {
97
+ success: false,
98
+ message: `Server category ${categoryName} has no feed configuration`,
99
+ };
100
+ }
101
+ // Find the requirement
102
+ const requirement = serverCategory.feedConfiguration.requirements.find(r => r.name === requirementName);
103
+ if (!requirement) {
104
+ return {
105
+ success: false,
106
+ message: `Requirement ${requirementName} not found in category ${categoryName}`,
107
+ };
108
+ }
109
+ // Update the requirement using requirementService
110
+ const updatedStatus = await requirementService.updateRequirement(requirement, updateVersion);
111
+ // Update the status in configuration
112
+ await this.configProvider.updateRequirementStatus(categoryName, requirementName, updatedStatus);
113
+ return {
114
+ success: true,
115
+ message: `Successfully updated ${requirementName} to version ${updateVersion}`,
116
+ };
117
+ }
118
+ catch (error) {
119
+ console.error(`Error updating requirement ${requirementName}:`, error);
120
+ return {
121
+ success: false,
122
+ message: `Failed to update ${requirementName}: ${error instanceof Error ? error.message : String(error)}`,
123
+ error: error instanceof Error ? error : new Error(String(error)),
124
+ };
125
+ }
126
+ }
85
127
  // Type-safe event emitter methods
86
128
  emit(event, data) {
87
129
  return super.emit(event, data);
@@ -29,6 +29,13 @@ export declare class RequirementService {
29
29
  * @returns Updated status with available updates information
30
30
  */
31
31
  checkRequirementForUpdates(requirement: RequirementConfig): Promise<RequirementStatus>;
32
+ /**
33
+ * Update a requirement to a new version
34
+ * @param requirement The requirement configuration
35
+ * @param updateVersion The version to update to
36
+ * @returns The updated requirement status
37
+ */
38
+ updateRequirement(requirement: RequirementConfig, updateVersion: string): Promise<RequirementStatus>;
32
39
  /**
33
40
  * Install multiple requirements
34
41
  * @param requirements The requirements to install
@@ -61,6 +61,23 @@ export class RequirementService {
61
61
  }
62
62
  return status;
63
63
  }
64
+ /**
65
+ * Update a requirement to a new version
66
+ * @param requirement The requirement configuration
67
+ * @param updateVersion The version to update to
68
+ * @returns The updated requirement status
69
+ */
70
+ async updateRequirement(requirement, updateVersion) {
71
+ // Validate requirement
72
+ this.validateRequirement(requirement);
73
+ // Create an updated requirement with the new version
74
+ const updatedRequirement = {
75
+ ...requirement,
76
+ version: updateVersion
77
+ };
78
+ // Install the updated version
79
+ return await this.installRequirement(updatedRequirement);
80
+ }
64
81
  /**
65
82
  * Install multiple requirements
66
83
  * @param requirements The requirements to install
@@ -27,3 +27,8 @@ export declare const SUPPORTED_CLIENTS: Record<string, any>;
27
27
  * List of supported client names.
28
28
  */
29
29
  export declare const SUPPORTED_CLIENT_NAMES: string[];
30
+ /**
31
+ * Minimum time between requirement update checks (in milliseconds)
32
+ * 10 minutes = 10 * 60 * 1000 = 600000 ms
33
+ */
34
+ export declare const UPDATE_CHECK_INTERVAL_MS: number;
@@ -34,14 +34,14 @@ const CODE_INSIDER_STRORAGE_DIR = path.join(os.homedir(), 'AppData', 'Roaming',
34
34
  * TODO: Define actual client settings structure.
35
35
  */
36
36
  export const SUPPORTED_CLIENTS = {
37
- 'Cline': {
38
- codeSettingPath: path.join(CODE_STRORAGE_DIR, 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'),
39
- codeInsiderSettingPath: path.join(CODE_INSIDER_STRORAGE_DIR, 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'),
40
- },
41
37
  'MSRooCode': {
42
38
  codeSettingPath: path.join(CODE_STRORAGE_DIR, 'globalStorage', 'microsoftai.ms-roo-cline', 'settings', 'cline_mcp_settings.json'),
43
39
  codeInsiderSettingPath: path.join(CODE_INSIDER_STRORAGE_DIR, 'globalStorage', 'microsoftai.ms-roo-clinev', 'settings', 'cline_mcp_settings.json'),
44
40
  },
41
+ 'Cline': {
42
+ codeSettingPath: path.join(CODE_STRORAGE_DIR, 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'),
43
+ codeInsiderSettingPath: path.join(CODE_INSIDER_STRORAGE_DIR, 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'),
44
+ },
45
45
  'GithubCopilot': {
46
46
  codeSettingPath: path.join(CODE_STRORAGE_DIR, 'settings.json'),
47
47
  codeInsiderSettingPath: path.join(CODE_INSIDER_STRORAGE_DIR, 'settings.json'),
@@ -52,4 +52,9 @@ export const SUPPORTED_CLIENTS = {
52
52
  * List of supported client names.
53
53
  */
54
54
  export const SUPPORTED_CLIENT_NAMES = Object.keys(SUPPORTED_CLIENTS);
55
+ /**
56
+ * Minimum time between requirement update checks (in milliseconds)
57
+ * 10 minutes = 10 * 60 * 1000 = 600000 ms
58
+ */
59
+ export const UPDATE_CHECK_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
55
60
  //# sourceMappingURL=constants.js.map
@@ -2,6 +2,7 @@ import path from 'path';
2
2
  import fs from 'fs/promises';
3
3
  import { SETTINGS_DIR } from '../constants.js';
4
4
  import { extractZipFile } from '../../utils/clientUtils.js';
5
+ import { Logger } from '../../utils/logger.js';
5
6
  /**
6
7
  * Abstract base class with common functionality for all requirement installers
7
8
  */
@@ -51,12 +52,17 @@ export class BaseInstaller {
51
52
  ...currentStatus,
52
53
  availableUpdate: {
53
54
  version: latestVersion,
54
- message: `Update available: ${currentStatus.version} → ${latestVersion}`,
55
- checkTime: new Date().toISOString()
56
- }
55
+ message: `Update available: ${currentStatus.version} → ${latestVersion}`
56
+ },
57
+ lastCheckTime: new Date().toISOString()
58
+ };
59
+ }
60
+ else {
61
+ return {
62
+ ...currentStatus,
63
+ lastCheckTime: new Date().toISOString()
57
64
  };
58
65
  }
59
- return currentStatus;
60
66
  }
61
67
  catch (error) {
62
68
  // Don't update status on error, just log it
@@ -88,31 +94,42 @@ export class BaseInstaller {
88
94
  let version = requirement.version;
89
95
  let resolvedAssetName = assetName || '';
90
96
  let resolvedAssetsName = assetsName || '';
97
+ const { stdout } = await this.execPromise(`gh release view --repo ${repository} --json tagName --jq .tagName`);
98
+ const latestTag = stdout.trim();
91
99
  // Handle latest version detection
92
100
  if (version.includes('${latest}') || version === 'latest') {
93
- const { stdout } = await this.execPromise(`gh release view --repo ${repository} --json tagName --jq .tagName`);
94
- const latestTag = stdout.trim();
95
101
  let latestVersion = latestTag;
96
102
  if (latestVersion.startsWith('v') && version.startsWith('v')) {
97
103
  latestVersion = latestVersion.substring(1); // Remove 'v' prefix if present
98
104
  // Replace ${latest} in version and asset names
99
105
  version = version.replace('${latest}', latestVersion);
100
106
  if (assetsName) {
101
- resolvedAssetsName = assetsName.replace('${latest}', latestVersion);
107
+ resolvedAssetsName = assetsName.replace('${latest}', latestVersion).replace('${version}', version);
102
108
  }
103
109
  if (assetName) {
104
- resolvedAssetName = assetName.replace('${latest}', latestVersion);
110
+ resolvedAssetName = assetName.replace('${latest}', latestVersion).replace('${version}', version);
105
111
  }
106
112
  }
107
113
  }
114
+ else {
115
+ if (assetsName) {
116
+ resolvedAssetsName = assetsName.replace('${latest}', version).replace('${version}', version);
117
+ }
118
+ if (assetName) {
119
+ resolvedAssetName = assetName.replace('${latest}', version).replace('${version}', version);
120
+ }
121
+ Logger.debug(`Downloading ${requirement.name} from GitHub release ${repository} version ${version}`);
122
+ Logger.debug(`ResolvedAssetsName} ${resolvedAssetName}; ResolvedAsetName} ${resolvedAssetName}`);
123
+ }
108
124
  const pattern = resolvedAssetsName ? resolvedAssetsName : resolvedAssetName;
125
+ Logger.debug(`Resolved pattern: ${pattern}`);
109
126
  if (!pattern) {
110
127
  throw new Error('Either assetsName or assetName must be specified for GitHub release downloads');
111
128
  }
112
129
  // Download the release asset
113
130
  const downloadPath = path.join(this.downloadsDir, path.basename(pattern));
114
131
  if (!await this.fileExists(downloadPath)) {
115
- await this.execPromise(`gh release download ${version} --repo ${repository} --pattern "${pattern}" -O "${downloadPath}"`);
132
+ await this.execPromise(`gh release download ${version.startsWith('v') ? version : `v${version}`} --repo ${repository} --pattern "${pattern}" -O "${downloadPath}"`);
116
133
  }
117
134
  // Handle zip file extraction if the downloaded file is a zip
118
135
  if (downloadPath.endsWith('.zip')) {
@@ -66,11 +66,6 @@ export class GeneralInstaller extends BaseInstaller {
66
66
  installed: true,
67
67
  version: requirement.version,
68
68
  inProgress: false,
69
- // Store installation path in a way that it can be retrieved later if needed
70
- updateInfo: {
71
- available: false,
72
- installPath
73
- }
74
69
  };
75
70
  }
76
71
  catch (error) {
@@ -1,4 +1,5 @@
1
1
  import { BaseInstaller } from './BaseInstaller.js';
2
+ import { compareVersions } from '../../utils/versionUtils.js';
2
3
  /**
3
4
  * Installer implementation for NPM packages
4
5
  */
@@ -47,7 +48,7 @@ export class NpmInstaller extends BaseInstaller {
47
48
  async install(requirement) {
48
49
  try {
49
50
  const status = await this.checkInstallation(requirement);
50
- if (status.installed) {
51
+ if (status.installed && status.version && compareVersions(status.version, requirement.version) === 0) {
51
52
  return status;
52
53
  }
53
54
  let resolvedVersion = requirement.version;
@@ -13,14 +13,9 @@ export interface RequirementStatus {
13
13
  availableUpdate?: {
14
14
  version: string;
15
15
  message: string;
16
- checkTime: string;
17
16
  };
17
+ lastCheckTime?: string;
18
18
  operationStatus?: OperationStatus;
19
- updateInfo?: {
20
- available: boolean;
21
- latestVersion?: string;
22
- [key: string]: any;
23
- } | null;
24
19
  }
25
20
  export interface MCPServerStatus {
26
21
  installedStatus: Record<string, OperationStatus>;
@@ -62,11 +57,17 @@ export interface ServerOperationResult {
62
57
  export interface MCPConfiguration {
63
58
  localServerCategories: MCPServerCategory[];
64
59
  feeds: Record<string, FeedConfiguration>;
60
+ clientMCPSettings?: Record<string, Record<string, any>>;
65
61
  }
66
62
  export interface ServerInstallOptions {
67
63
  force?: boolean;
68
64
  env?: Record<string, string>;
69
65
  targetClients?: string[];
66
+ requirements?: RequirementConfig[];
67
+ }
68
+ export interface UpdateRequirementOptions {
69
+ requirementName: string;
70
+ updateVersion: string;
70
71
  }
71
72
  export interface ServerUninstallOptions {
72
73
  removeData?: boolean;
@@ -2,6 +2,7 @@ import path from 'path';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { Logger } from '../utils/logger.js';
4
4
  import { mcpManager } from '../core/MCPManager.js';
5
+ import { UPDATE_CHECK_INTERVAL_MS } from '../core/constants.js';
5
6
  import { updateCheckTracker } from '../utils/UpdateCheckTracker.js';
6
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
8
  /**
@@ -59,6 +60,16 @@ export class ServerService {
59
60
  const currentStatus = serverCategory.installationStatus?.requirementsStatus[requirement.name];
60
61
  if (!currentStatus)
61
62
  continue;
63
+ // Skip update check if last check was less than UPDATE_CHECK_INTERVAL_MS ago
64
+ if (currentStatus.lastCheckTime) {
65
+ const lastCheckTime = new Date(currentStatus.lastCheckTime);
66
+ const currentTime = new Date();
67
+ const timeSinceLastCheck = currentTime.getTime() - lastCheckTime.getTime();
68
+ if (timeSinceLastCheck < UPDATE_CHECK_INTERVAL_MS) {
69
+ Logger.debug(`Skipping update check for ${requirement.name}, last check was ${Math.round(timeSinceLastCheck / 1000)} seconds ago`);
70
+ continue;
71
+ }
72
+ }
62
73
  // Check for updates
63
74
  const updatedStatus = await requirementService.checkRequirementForUpdates(requirement);
64
75
  // If update information is found, update the configuration
@@ -69,6 +80,11 @@ export class ServerService {
69
80
  serverCategory.installationStatus.requirementsStatus[requirement.name] = updatedStatus;
70
81
  }
71
82
  }
83
+ currentStatus.lastCheckTime = new Date().toISOString();
84
+ await configProvider.updateRequirementStatus(serverCategory.name, requirement.name, currentStatus);
85
+ if (serverCategory.installationStatus?.requirementsStatus) {
86
+ serverCategory.installationStatus.requirementsStatus[requirement.name] = updatedStatus;
87
+ }
72
88
  }
73
89
  }
74
90
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Utility functions for version comparison and management
3
+ */
4
+ /**
5
+ * Compare two semantic version strings
6
+ * @param v1 First version
7
+ * @param v2 Second version
8
+ * @returns -1 if v1 < v2, 0 if v1 = v2, 1 if v1 > v2
9
+ * (or more specifically, a negative number if v1 < v2,
10
+ * a positive number if v1 > v2, 0 if equal)
11
+ */
12
+ export declare function compareVersions(v1: string, v2: string): number;