imcp 0.0.1

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 (124) hide show
  1. package/.github/ISSUE_TEMPLATE/JitAccess.yml +28 -0
  2. package/.github/acl/access.yml +20 -0
  3. package/.github/compliance/inventory.yml +5 -0
  4. package/.github/policies/jit.yml +19 -0
  5. package/README.md +137 -0
  6. package/dist/cli/commands/install.d.ts +2 -0
  7. package/dist/cli/commands/install.js +105 -0
  8. package/dist/cli/commands/list.d.ts +2 -0
  9. package/dist/cli/commands/list.js +90 -0
  10. package/dist/cli/commands/pull.d.ts +2 -0
  11. package/dist/cli/commands/pull.js +17 -0
  12. package/dist/cli/commands/serve.d.ts +2 -0
  13. package/dist/cli/commands/serve.js +32 -0
  14. package/dist/cli/commands/start.d.ts +2 -0
  15. package/dist/cli/commands/start.js +32 -0
  16. package/dist/cli/commands/sync.d.ts +2 -0
  17. package/dist/cli/commands/sync.js +17 -0
  18. package/dist/cli/commands/uninstall.d.ts +2 -0
  19. package/dist/cli/commands/uninstall.js +39 -0
  20. package/dist/cli/index.d.ts +2 -0
  21. package/dist/cli/index.js +114 -0
  22. package/dist/core/ConfigurationProvider.d.ts +31 -0
  23. package/dist/core/ConfigurationProvider.js +416 -0
  24. package/dist/core/InstallationService.d.ts +17 -0
  25. package/dist/core/InstallationService.js +144 -0
  26. package/dist/core/MCPManager.d.ts +17 -0
  27. package/dist/core/MCPManager.js +98 -0
  28. package/dist/core/RequirementService.d.ts +45 -0
  29. package/dist/core/RequirementService.js +123 -0
  30. package/dist/core/constants.d.ts +29 -0
  31. package/dist/core/constants.js +55 -0
  32. package/dist/core/installers/BaseInstaller.d.ts +73 -0
  33. package/dist/core/installers/BaseInstaller.js +247 -0
  34. package/dist/core/installers/ClientInstaller.d.ts +17 -0
  35. package/dist/core/installers/ClientInstaller.js +307 -0
  36. package/dist/core/installers/CommandInstaller.d.ts +36 -0
  37. package/dist/core/installers/CommandInstaller.js +170 -0
  38. package/dist/core/installers/GeneralInstaller.d.ts +32 -0
  39. package/dist/core/installers/GeneralInstaller.js +87 -0
  40. package/dist/core/installers/InstallerFactory.d.ts +52 -0
  41. package/dist/core/installers/InstallerFactory.js +95 -0
  42. package/dist/core/installers/NpmInstaller.d.ts +25 -0
  43. package/dist/core/installers/NpmInstaller.js +123 -0
  44. package/dist/core/installers/PipInstaller.d.ts +25 -0
  45. package/dist/core/installers/PipInstaller.js +114 -0
  46. package/dist/core/installers/RequirementInstaller.d.ts +32 -0
  47. package/dist/core/installers/RequirementInstaller.js +3 -0
  48. package/dist/core/installers/index.d.ts +6 -0
  49. package/dist/core/installers/index.js +7 -0
  50. package/dist/core/types.d.ts +152 -0
  51. package/dist/core/types.js +16 -0
  52. package/dist/index.d.ts +11 -0
  53. package/dist/index.js +19 -0
  54. package/dist/services/InstallRequestValidator.d.ts +21 -0
  55. package/dist/services/InstallRequestValidator.js +99 -0
  56. package/dist/services/ServerService.d.ts +47 -0
  57. package/dist/services/ServerService.js +145 -0
  58. package/dist/utils/UpdateCheckTracker.d.ts +39 -0
  59. package/dist/utils/UpdateCheckTracker.js +80 -0
  60. package/dist/utils/clientUtils.d.ts +29 -0
  61. package/dist/utils/clientUtils.js +105 -0
  62. package/dist/utils/feedUtils.d.ts +5 -0
  63. package/dist/utils/feedUtils.js +29 -0
  64. package/dist/utils/githubAuth.d.ts +1 -0
  65. package/dist/utils/githubAuth.js +123 -0
  66. package/dist/utils/logger.d.ts +14 -0
  67. package/dist/utils/logger.js +90 -0
  68. package/dist/utils/osUtils.d.ts +16 -0
  69. package/dist/utils/osUtils.js +235 -0
  70. package/dist/web/public/css/modal.css +250 -0
  71. package/dist/web/public/css/notifications.css +70 -0
  72. package/dist/web/public/index.html +157 -0
  73. package/dist/web/public/js/api.js +213 -0
  74. package/dist/web/public/js/modal.js +572 -0
  75. package/dist/web/public/js/notifications.js +99 -0
  76. package/dist/web/public/js/serverCategoryDetails.js +210 -0
  77. package/dist/web/public/js/serverCategoryList.js +82 -0
  78. package/dist/web/public/modal.html +61 -0
  79. package/dist/web/public/styles.css +155 -0
  80. package/dist/web/server.d.ts +5 -0
  81. package/dist/web/server.js +150 -0
  82. package/package.json +53 -0
  83. package/src/cli/commands/install.ts +140 -0
  84. package/src/cli/commands/list.ts +112 -0
  85. package/src/cli/commands/pull.ts +16 -0
  86. package/src/cli/commands/serve.ts +37 -0
  87. package/src/cli/commands/uninstall.ts +54 -0
  88. package/src/cli/index.ts +127 -0
  89. package/src/core/ConfigurationProvider.ts +489 -0
  90. package/src/core/InstallationService.ts +173 -0
  91. package/src/core/MCPManager.ts +134 -0
  92. package/src/core/RequirementService.ts +147 -0
  93. package/src/core/constants.ts +61 -0
  94. package/src/core/installers/BaseInstaller.ts +292 -0
  95. package/src/core/installers/ClientInstaller.ts +423 -0
  96. package/src/core/installers/CommandInstaller.ts +185 -0
  97. package/src/core/installers/GeneralInstaller.ts +89 -0
  98. package/src/core/installers/InstallerFactory.ts +109 -0
  99. package/src/core/installers/NpmInstaller.ts +128 -0
  100. package/src/core/installers/PipInstaller.ts +121 -0
  101. package/src/core/installers/RequirementInstaller.ts +38 -0
  102. package/src/core/installers/index.ts +9 -0
  103. package/src/core/types.ts +163 -0
  104. package/src/index.ts +44 -0
  105. package/src/services/InstallRequestValidator.ts +112 -0
  106. package/src/services/ServerService.ts +181 -0
  107. package/src/utils/UpdateCheckTracker.ts +86 -0
  108. package/src/utils/clientUtils.ts +112 -0
  109. package/src/utils/feedUtils.ts +31 -0
  110. package/src/utils/githubAuth.ts +142 -0
  111. package/src/utils/logger.ts +101 -0
  112. package/src/utils/osUtils.ts +250 -0
  113. package/src/web/public/css/modal.css +250 -0
  114. package/src/web/public/css/notifications.css +70 -0
  115. package/src/web/public/index.html +157 -0
  116. package/src/web/public/js/api.js +213 -0
  117. package/src/web/public/js/modal.js +572 -0
  118. package/src/web/public/js/notifications.js +99 -0
  119. package/src/web/public/js/serverCategoryDetails.js +210 -0
  120. package/src/web/public/js/serverCategoryList.js +82 -0
  121. package/src/web/public/modal.html +61 -0
  122. package/src/web/public/styles.css +155 -0
  123. package/src/web/server.ts +195 -0
  124. package/tsconfig.json +18 -0
@@ -0,0 +1,489 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { exec } from 'child_process';
5
+ import { promisify } from 'util';
6
+ import { fileURLToPath } from 'url';
7
+ import { GITHUB_REPO, LOCAL_FEEDS_DIR, SETTINGS_DIR } from './constants.js';
8
+ import { Logger } from '../utils/logger.js';
9
+ import { checkGithubAuth } from '../utils/githubAuth.js';
10
+ import {
11
+ MCPConfiguration,
12
+ MCPServerCategory,
13
+ FeedConfiguration,
14
+ InstallationStatus,
15
+ RequirementStatus,
16
+ MCPServerStatus,
17
+ OperationStatus
18
+ } from './types.js';
19
+
20
+ const execAsync = promisify(exec);
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+
23
+ export class ConfigurationProvider {
24
+ private static instance: ConfigurationProvider;
25
+ private configPath: string;
26
+ private configuration: MCPConfiguration;
27
+ private configLock: Promise<void> = Promise.resolve();
28
+ private tempDir: string;
29
+
30
+ private constructor() {
31
+ // Initialize configuration in user's appdata/imcp directory
32
+ this.configPath = path.join(SETTINGS_DIR, 'configurations.json');
33
+ this.configuration = {
34
+ localServerCategories: [],
35
+ feeds: {},
36
+ };
37
+ this.tempDir = path.join(LOCAL_FEEDS_DIR, '../temp');
38
+ }
39
+
40
+ public static getInstance(): ConfigurationProvider {
41
+ if (!ConfigurationProvider.instance) {
42
+ ConfigurationProvider.instance = new ConfigurationProvider();
43
+ }
44
+ return ConfigurationProvider.instance;
45
+ }
46
+
47
+ private async withLock<T>(operation: () => Promise<T>): Promise<T> {
48
+ const current = this.configLock;
49
+ let resolve: () => void;
50
+ this.configLock = new Promise<void>(r => resolve = r);
51
+ try {
52
+ await current;
53
+ return await operation();
54
+ } finally {
55
+ resolve!();
56
+ }
57
+ }
58
+
59
+ async initialize(): Promise<void> {
60
+ await this.withLock(async () => {
61
+ const configDir = path.dirname(this.configPath);
62
+ await fs.mkdir(configDir, { recursive: true });
63
+
64
+ try {
65
+ const config = JSON.parse(await fs.readFile(this.configPath, 'utf8'));
66
+ this.configuration = config;
67
+ await this.loadFeedsIntoConfiguration(); // Load feeds into configuration
68
+ } catch (error) {
69
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
70
+ throw error;
71
+ }
72
+ // File doesn't exist, use default empty configuration
73
+ await this.saveConfiguration();
74
+ }
75
+ });
76
+ }
77
+
78
+ private async saveConfiguration(): Promise<void> {
79
+ const configDir = path.dirname(this.configPath);
80
+ await fs.mkdir(configDir, { recursive: true });
81
+ await fs.writeFile(this.configPath, JSON.stringify(this.configuration, null, 2));
82
+ }
83
+
84
+ async getServerCategories(): Promise<MCPServerCategory[]> {
85
+ return await this.withLock(async () => {
86
+ return this.configuration.localServerCategories;
87
+ });
88
+ }
89
+
90
+ async getServerCategory(categoryName: string): Promise<MCPServerCategory | undefined> {
91
+ return await this.withLock(async () => {
92
+ return this.configuration.localServerCategories.find(s => s.name === categoryName);
93
+ });
94
+ }
95
+
96
+ async getFeedConfiguration(categoryName: string): Promise<FeedConfiguration | undefined> {
97
+ return await this.withLock(async () => {
98
+ return this.configuration.feeds[categoryName];
99
+ });
100
+ }
101
+
102
+ async getInstallationStatus(categoryName: string): Promise<InstallationStatus | undefined> {
103
+ return await this.withLock(async () => {
104
+ // Inline getServerCategory logic
105
+ const category = this.configuration.localServerCategories.find(s => s.name === categoryName);
106
+ return category?.installationStatus;
107
+ });
108
+ }
109
+
110
+ async getServerStatus(categoryName: string, serverName: string): Promise<MCPServerStatus | undefined> {
111
+ return await this.withLock(async () => {
112
+ // Inline getInstallationStatus logic
113
+ const category = this.configuration.localServerCategories.find(s => s.name === categoryName);
114
+ const status = category?.installationStatus;
115
+ return status?.serversStatus[serverName];
116
+ });
117
+ }
118
+
119
+ async getRequirementStatus(categoryName: string, requirementName: string): Promise<RequirementStatus | undefined> {
120
+ return await this.withLock(async () => {
121
+ // Inline getInstallationStatus logic
122
+ const category = this.configuration.localServerCategories.find(s => s.name === categoryName);
123
+ const status = category?.installationStatus;
124
+ return status?.requirementsStatus[requirementName];
125
+ });
126
+ }
127
+
128
+ async updateInstallationStatus(
129
+ categoryName: string,
130
+ requirementStatus: Record<string, RequirementStatus>,
131
+ serverStatus: Record<string, MCPServerStatus>
132
+ ): Promise<boolean> {
133
+ return await this.withLock(async () => {
134
+ // Inline getServerCategory logic
135
+ const category = this.configuration.localServerCategories.find(s => s.name === categoryName);
136
+ if (!category) return false;
137
+
138
+ if (!category.installationStatus) {
139
+ category.installationStatus = {
140
+ requirementsStatus: {},
141
+ serversStatus: {},
142
+ lastUpdated: new Date().toISOString()
143
+ };
144
+ }
145
+
146
+ category.installationStatus.requirementsStatus = {
147
+ ...category.installationStatus.requirementsStatus,
148
+ ...requirementStatus
149
+ };
150
+
151
+ category.installationStatus.serversStatus = {
152
+ ...category.installationStatus.serversStatus,
153
+ ...serverStatus
154
+ };
155
+
156
+ category.installationStatus.lastUpdated = new Date().toISOString();
157
+ await this.saveConfiguration();
158
+ return true;
159
+ });
160
+ }
161
+
162
+ async updateRequirementStatus(
163
+ categoryName: string,
164
+ requirementName: string,
165
+ status: RequirementStatus
166
+ ): Promise<boolean> {
167
+ return await this.withLock(async () => {
168
+ // Inline getServerCategory logic
169
+ const category = this.configuration.localServerCategories.find(s => s.name === categoryName);
170
+ if (!category?.installationStatus) return false;
171
+
172
+ category.installationStatus.requirementsStatus[requirementName] = status;
173
+ category.installationStatus.lastUpdated = new Date().toISOString();
174
+ await this.saveConfiguration();
175
+ return true;
176
+ });
177
+ }
178
+
179
+ async updateRequirementOperationStatus(
180
+ categoryName: string,
181
+ requirementName: string,
182
+ operationStatus: OperationStatus
183
+ ): Promise<boolean> {
184
+ return await this.withLock(async () => {
185
+ // Inline getServerCategory logic
186
+ const category = this.configuration.localServerCategories.find(s => s.name === categoryName);
187
+ if (!category?.installationStatus?.requirementsStatus[requirementName]) return false;
188
+
189
+ category.installationStatus.requirementsStatus[requirementName].operationStatus = operationStatus;
190
+ category.installationStatus.lastUpdated = new Date().toISOString();
191
+ await this.saveConfiguration();
192
+ return true;
193
+ });
194
+ }
195
+
196
+ async updateServerStatus(
197
+ categoryName: string,
198
+ serverName: string,
199
+ status: MCPServerStatus
200
+ ): Promise<boolean> {
201
+ return await this.withLock(async () => {
202
+ // Inline getServerCategory logic
203
+ const category = this.configuration.localServerCategories.find(s => s.name === categoryName);
204
+ if (!category?.installationStatus) return false;
205
+
206
+ category.installationStatus.serversStatus[serverName] = status;
207
+ category.installationStatus.lastUpdated = new Date().toISOString();
208
+ await this.saveConfiguration();
209
+ return true;
210
+ });
211
+ }
212
+
213
+ async updateServerOperationStatus(
214
+ categoryName: string,
215
+ serverName: string,
216
+ clientName: string,
217
+ operationStatus: OperationStatus
218
+ ): Promise<boolean> {
219
+ return await this.withLock(async () => {
220
+ // Inline getServerCategory logic
221
+ const category = this.configuration.localServerCategories.find(s => s.name === categoryName);
222
+ if (!category?.installationStatus?.serversStatus[serverName]) return false;
223
+
224
+ category.installationStatus.serversStatus[serverName].installedStatus[clientName] = operationStatus;
225
+ category.installationStatus.lastUpdated = new Date().toISOString();
226
+ await this.saveConfiguration();
227
+ return true;
228
+ });
229
+ }
230
+
231
+ async isRequirementsReady(categoryName: string, serverName: string): Promise<boolean> {
232
+ return await this.withLock(async () => {
233
+ // Inline getServerCategory logic
234
+ const category = this.configuration.localServerCategories.find(s => s.name === categoryName);
235
+ if (!category?.feedConfiguration) return false;
236
+
237
+ const serverConfig = category.feedConfiguration.mcpServers.find(s => s.name === serverName);
238
+ if (!serverConfig?.dependencies?.requirements) return true; // No requirements means ready
239
+
240
+ const requirementNames = serverConfig.dependencies.requirements.map(r => r.name);
241
+ // Inline getInstallationStatus logic (using the already fetched category)
242
+ const status = category?.installationStatus;
243
+
244
+ if (!status?.requirementsStatus) return false;
245
+
246
+ return requirementNames.every(name => {
247
+ const reqStatus = status.requirementsStatus[name];
248
+ return reqStatus?.installed && !reqStatus?.error;
249
+ });
250
+ });
251
+ }
252
+
253
+ async isServerReady(categoryName: string, serverName: string, clients: string[]): Promise<boolean> {
254
+ return await this.withLock(async () => {
255
+ // Inline the logic from getServerStatus and getInstallationStatus to avoid nested lock
256
+ const category = this.configuration.localServerCategories.find(s => s.name === categoryName);
257
+ const installationStatus = category?.installationStatus;
258
+ const serverStatus = installationStatus?.serversStatus[serverName];
259
+
260
+ if (!serverStatus) return false;
261
+
262
+ return clients.every(clientName => {
263
+ // Add optional chaining for safety in case installedStatus is missing
264
+ const clientStatus = serverStatus.installedStatus?.[clientName];
265
+ return clientStatus?.status === 'completed' && !clientStatus?.error;
266
+ });
267
+ });
268
+ }
269
+ async syncFeeds(): Promise<void> {
270
+ return await this.withLock(async () => {
271
+ Logger.log('Starting feed synchronization...');
272
+ try {
273
+ // Check GitHub authentication first
274
+ await checkGithubAuth();
275
+
276
+ Logger.debug({
277
+ action: 'create_directories',
278
+ paths: {
279
+ localFeeds: LOCAL_FEEDS_DIR,
280
+ tempDir: this.tempDir
281
+ }
282
+ });
283
+
284
+ await fs.mkdir(LOCAL_FEEDS_DIR, { recursive: true });
285
+ await fs.mkdir(this.tempDir, { recursive: true });
286
+
287
+ try {
288
+ await fs.access(path.join(this.tempDir, '.git'));
289
+ Logger.debug('Found existing repository, updating...');
290
+ const { stdout, stderr } = await execAsync('git pull', { cwd: this.tempDir });
291
+ Logger.debug({
292
+ action: 'git_pull',
293
+ stderr,
294
+ stdout
295
+ });
296
+ } catch (err) {
297
+ Logger.debug('No existing repository found, cloning...');
298
+ await fs.rm(this.tempDir, { recursive: true, force: true });
299
+ const { stdout, stderr } = await execAsync(`git clone ${GITHUB_REPO.url} ${this.tempDir}`);
300
+ Logger.debug({
301
+ action: 'git_clone',
302
+ stderr,
303
+ stdout,
304
+ url: GITHUB_REPO.url
305
+ });
306
+ }
307
+
308
+ Logger.debug('Updating local feeds...');
309
+ await fs.rm(LOCAL_FEEDS_DIR, { recursive: true, force: true });
310
+ const sourceFeedsDir = path.join(this.tempDir, GITHUB_REPO.feedsPath);
311
+
312
+ try {
313
+ await fs.access(sourceFeedsDir);
314
+ } catch (err) {
315
+ throw new Error(`Could not find feeds directory in cloned repository: ${sourceFeedsDir}`);
316
+ }
317
+
318
+ await fs.cp(sourceFeedsDir, LOCAL_FEEDS_DIR, { recursive: true });
319
+ Logger.log('Successfully updated local feeds');
320
+
321
+ // Update configuration with new feeds
322
+ await this.loadFeedsIntoConfiguration();
323
+ } catch (error) {
324
+ Logger.error('Error during feed synchronization', error);
325
+ throw new Error('Failed to sync feeds. Use --verbose for detailed error information.');
326
+ }
327
+ });
328
+ }
329
+
330
+ private async loadFeedsIntoConfiguration(): Promise<void> {
331
+ try {
332
+ await fs.mkdir(LOCAL_FEEDS_DIR, { recursive: true });
333
+ const files = await fs.readdir(LOCAL_FEEDS_DIR);
334
+ const jsonFiles = files.filter(file => file.endsWith('.json'));
335
+
336
+ if (jsonFiles.length === 0) {
337
+ console.log(`No feed configuration files found in ${LOCAL_FEEDS_DIR}`);
338
+ return;
339
+ }
340
+
341
+ const feeds: Record<string, FeedConfiguration> = {};
342
+ for (const file of jsonFiles) {
343
+ try {
344
+ const filePath = path.join(LOCAL_FEEDS_DIR, file);
345
+ const content = await fs.readFile(filePath, 'utf8');
346
+ const config = JSON.parse(content) as FeedConfiguration;
347
+ if (config && config.name) {
348
+ feeds[config.name] = config;
349
+ }
350
+ } catch (error) {
351
+ console.warn(`Error loading feed configuration from ${file}:`, error);
352
+ }
353
+ }
354
+
355
+ this.configuration.feeds = feeds;
356
+ await this.syncServerCategoriesWithFeeds(); // Sync categories after loading feeds
357
+ await this.saveConfiguration();
358
+ } catch (error) {
359
+ console.error("Error loading feed configurations:", error);
360
+ throw error;
361
+ }
362
+ }
363
+
364
+ private async syncServerCategoriesWithFeeds(): Promise<void> {
365
+ let configUpdated = false;
366
+
367
+ // 1. Process existing local servers - update their feed configurations
368
+ for (const server of this.configuration.localServerCategories) {
369
+ if (this.configuration.feeds[server.name]) {
370
+ server.feedConfiguration = this.configuration.feeds[server.name];
371
+ configUpdated = true;
372
+ }
373
+
374
+ // If server doesn't have installation status, initialize it
375
+ const feedConfig = server.feedConfiguration;
376
+ // If installationStatus is missing, or requirements/tools are empty, initialize from feed
377
+ if (
378
+ !server.installationStatus ||
379
+ !server.installationStatus.requirementsStatus ||
380
+ Object.keys(server.installationStatus.requirementsStatus).length === 0 ||
381
+ !server.installationStatus.serversStatus ||
382
+ Object.keys(server.installationStatus.serversStatus).length === 0
383
+ ) {
384
+ const requirementsStatus: Record<string, RequirementStatus> = {};
385
+ const serversStatus: Record<string, MCPServerStatus> = {};
386
+ if (feedConfig) {
387
+ if (feedConfig.requirements) {
388
+ for (const req of feedConfig.requirements) {
389
+ requirementsStatus[req.name] = {
390
+ name: req.name,
391
+ type: req.type,
392
+ installed: false,
393
+ version: req.version,
394
+ error: undefined,
395
+ updateInfo: null
396
+ };
397
+ }
398
+ }
399
+ if (feedConfig.mcpServers) {
400
+ for (const mcp of feedConfig.mcpServers) {
401
+ serversStatus[mcp.name] = {
402
+ name: mcp.name,
403
+ error: undefined,
404
+ installedStatus: {} // Add missing property
405
+ };
406
+ }
407
+ }
408
+ }
409
+ server.installationStatus = {
410
+ requirementsStatus,
411
+ serversStatus,
412
+ lastUpdated: new Date().toISOString()
413
+ };
414
+ configUpdated = true;
415
+ }
416
+ }
417
+
418
+ // 2. Check for feeds that don't have a corresponding local server and create new entries
419
+ const existingServerCategoryNames = new Set(this.configuration.localServerCategories.map(catetory => catetory.name));
420
+
421
+ for (const feedName in this.configuration.feeds) {
422
+ if (!existingServerCategoryNames.has(feedName)) {
423
+ // This feed doesn't have a corresponding local server - create one with empty installation status
424
+ const feedConfig = this.configuration.feeds[feedName];
425
+
426
+ // Create new server with empty installation status
427
+ const newServerCategory: MCPServerCategory = {
428
+ name: feedName,
429
+ displayName: feedConfig.displayName || feedName,
430
+ type: 'local',
431
+ description: feedConfig.description || `Local MCP server category: ${feedName}`,
432
+ installationStatus: (() => {
433
+ const requirementsStatus: Record<string, RequirementStatus> = {};
434
+ const serversStatus: Record<string, MCPServerStatus> = {};
435
+ if (feedConfig) {
436
+ if (feedConfig.requirements) {
437
+ for (const req of feedConfig.requirements) {
438
+ requirementsStatus[req.name] = {
439
+ name: req.name,
440
+ type: req.type,
441
+ installed: false,
442
+ version: req.version,
443
+ error: undefined
444
+ };
445
+ }
446
+ }
447
+ if (feedConfig.mcpServers) {
448
+ for (const mcp of feedConfig.mcpServers) {
449
+ serversStatus[mcp.name] = {
450
+ name: mcp.name,
451
+ error: undefined,
452
+ installedStatus: {} // Add missing property
453
+ };
454
+ }
455
+ }
456
+ }
457
+ return {
458
+ requirementsStatus,
459
+ serversStatus,
460
+ lastUpdated: new Date().toISOString()
461
+ };
462
+ })(),
463
+ feedConfiguration: feedConfig
464
+ };
465
+
466
+ // Add the new server to the configuration
467
+ this.configuration.localServerCategories.push(newServerCategory);
468
+ console.log(`Created new local server entry for feed: ${feedName}`);
469
+ configUpdated = true;
470
+ }
471
+ }
472
+
473
+ if (configUpdated) {
474
+ await this.saveConfiguration();
475
+ }
476
+ }
477
+
478
+
479
+ async syncWithFeed(feedConfiguration: Record<string, FeedConfiguration>): Promise<void> {
480
+ await this.withLock(async () => {
481
+ this.configuration.feeds = feedConfiguration;
482
+ await this.syncServerCategoriesWithFeeds(); // Sync categories after direct update
483
+ await this.saveConfiguration();
484
+ });
485
+ }
486
+ }
487
+
488
+ // Export a singleton instance
489
+ export const configProvider = ConfigurationProvider.getInstance();
@@ -0,0 +1,173 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { exec } from 'child_process';
5
+ import util from 'util';
6
+ import {
7
+ ServerInstallOptions,
8
+ ServerOperationResult,
9
+ FeedConfiguration,
10
+ RequirementConfig,
11
+ OperationStatus,
12
+ RequirementStatus,
13
+ McpConfig
14
+ } from './types.js';
15
+ import { RequirementInstaller, InstallerFactory, createInstallerFactory } from './installers/index.js';
16
+ import { SUPPORTED_CLIENTS } from './constants.js';
17
+ import { ClientInstaller } from './installers/ClientInstaller.js';
18
+ import { ConfigurationProvider } from './ConfigurationProvider.js';
19
+
20
+ const execPromise = util.promisify(exec);
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+
23
+ /**
24
+ * Handles the actual installation process for an MCP server.
25
+ */
26
+
27
+ export class InstallationService {
28
+ private activeInstallations: Map<string, OperationStatus> = new Map();
29
+ private installerFactory: InstallerFactory;
30
+
31
+ constructor() {
32
+ this.installerFactory = createInstallerFactory();
33
+ }
34
+
35
+ private generateOperationId(): string {
36
+ return `install-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
37
+ }
38
+
39
+
40
+ /**
41
+ * Installs a server based on the provided options and feed configuration.
42
+ * @param serverName The name of the server to install.
43
+ * @param options The installation options.
44
+ * @returns A result object indicating success or failure.
45
+ */
46
+ async install(categoryName: string, serverName: string, options: ServerInstallOptions): Promise<ServerOperationResult> {
47
+ const configProvider = ConfigurationProvider.getInstance();
48
+ const clients = options.targetClients || Object.keys(SUPPORTED_CLIENTS);
49
+
50
+ // Check if server is already ready
51
+ const isReady = await configProvider.isServerReady(categoryName, serverName, clients);
52
+ if (isReady) {
53
+ return {
54
+ success: true,
55
+ message: 'Server and clients are already installed and ready',
56
+ status: [{
57
+ status: 'completed',
58
+ type: 'install',
59
+ target: 'server',
60
+ message: 'Server and clients are already installed and ready'
61
+ }]
62
+ };
63
+ }
64
+
65
+ // Create new ClientInstaller instance for handling installation
66
+ const clientInstaller = new ClientInstaller(categoryName, serverName, clients);
67
+
68
+ // Check requirements readiness
69
+ const requirementsReady = await configProvider.isRequirementsReady(categoryName, serverName);
70
+ if (!requirementsReady) {
71
+ // Get feed configuration to get requirements
72
+ const feedConfig = await configProvider.getFeedConfiguration(categoryName);
73
+ if (!feedConfig) {
74
+ return {
75
+ success: false,
76
+ message: 'Feed configuration not found',
77
+ status: [{
78
+ status: 'failed',
79
+ type: 'install',
80
+ target: 'server',
81
+ message: 'Feed configuration not found'
82
+ }]
83
+ };
84
+ }
85
+
86
+ // Find server config
87
+ const serverConfig = feedConfig.mcpServers.find((s: McpConfig) => s.name === serverName);
88
+ if (!serverConfig?.dependencies?.requirements) {
89
+ return {
90
+ success: false,
91
+ message: 'Server configuration or requirements not found',
92
+ status: [{
93
+ status: 'failed',
94
+ type: 'install',
95
+ target: 'server',
96
+ message: 'Server configuration or requirements not found'
97
+ }]
98
+ };
99
+ }
100
+
101
+ // Install requirements asynchronously
102
+ for (const requirement of serverConfig.dependencies.requirements) {
103
+ // Create full RequirementConfig from dependency requirement
104
+
105
+ const feeds = await configProvider.getFeedConfiguration(categoryName);
106
+ const requirementConfig = feeds?.requirements?.find((r: RequirementConfig) => r.name === requirement.name) || {
107
+ name: requirement.name,
108
+ version: requirement.version,
109
+ type: 'npm' // Default to npm, can be enhanced to determine from requirement or config
110
+ };
111
+
112
+ const installer = this.installerFactory.getInstaller(requirementConfig);
113
+ if (!installer) {
114
+ await configProvider.updateRequirementStatus(categoryName, requirement.name, {
115
+ name: requirement.name,
116
+ type: requirementConfig.type,
117
+ installed: false,
118
+ error: `No installer found for requirement type: ${requirementConfig.type}`,
119
+ operationStatus: {
120
+ status: 'failed',
121
+ type: 'install',
122
+ target: 'requirement',
123
+ message: `No installer found for requirement type: ${requirementConfig.type}`,
124
+ operationId: this.generateOperationId()
125
+ }
126
+ });
127
+ continue;
128
+ }
129
+
130
+ // Create operation status for requirement
131
+ const operationStatus: OperationStatus = {
132
+ status: 'pending',
133
+ type: 'install',
134
+ target: 'requirement',
135
+ message: `Installing requirement: ${requirement.name}`,
136
+ operationId: this.generateOperationId()
137
+ };
138
+
139
+ // Update requirement status
140
+ await configProvider.updateRequirementStatus(categoryName, requirement.name, {
141
+ name: requirement.name,
142
+ type: requirementConfig.type,
143
+ installed: false,
144
+ inProgress: true,
145
+ operationStatus
146
+ });
147
+
148
+ // Start async installation
149
+ installer.install(requirementConfig).then(async (installStatus) => {
150
+ const status: RequirementStatus = {
151
+ ...installStatus,
152
+ operationStatus: {
153
+ status: installStatus.installed ? 'completed' : 'failed',
154
+ type: 'install',
155
+ target: 'requirement',
156
+ message: installStatus.installed ? `Requirement ${requirement.name} installed successfully` : `Failed to install ${requirement.name}`,
157
+ operationId: operationStatus.operationId
158
+ }
159
+ };
160
+ await configProvider.updateRequirementStatus(categoryName, requirement.name, status);
161
+ });
162
+ }
163
+ }
164
+
165
+ // Process client installation regardless of requirements state
166
+ // Each client installer will check requirements before actual installation
167
+ return await clientInstaller.install(options);
168
+ }
169
+
170
+ }
171
+
172
+ // Export a singleton instance (optional)
173
+ // export const installationService = new InstallationService();