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.
- package/.github/ISSUE_TEMPLATE/JitAccess.yml +28 -0
- package/.github/acl/access.yml +20 -0
- package/.github/compliance/inventory.yml +5 -0
- package/.github/policies/jit.yml +19 -0
- package/README.md +137 -0
- package/dist/cli/commands/install.d.ts +2 -0
- package/dist/cli/commands/install.js +105 -0
- package/dist/cli/commands/list.d.ts +2 -0
- package/dist/cli/commands/list.js +90 -0
- package/dist/cli/commands/pull.d.ts +2 -0
- package/dist/cli/commands/pull.js +17 -0
- package/dist/cli/commands/serve.d.ts +2 -0
- package/dist/cli/commands/serve.js +32 -0
- package/dist/cli/commands/start.d.ts +2 -0
- package/dist/cli/commands/start.js +32 -0
- package/dist/cli/commands/sync.d.ts +2 -0
- package/dist/cli/commands/sync.js +17 -0
- package/dist/cli/commands/uninstall.d.ts +2 -0
- package/dist/cli/commands/uninstall.js +39 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +114 -0
- package/dist/core/ConfigurationProvider.d.ts +31 -0
- package/dist/core/ConfigurationProvider.js +416 -0
- package/dist/core/InstallationService.d.ts +17 -0
- package/dist/core/InstallationService.js +144 -0
- package/dist/core/MCPManager.d.ts +17 -0
- package/dist/core/MCPManager.js +98 -0
- package/dist/core/RequirementService.d.ts +45 -0
- package/dist/core/RequirementService.js +123 -0
- package/dist/core/constants.d.ts +29 -0
- package/dist/core/constants.js +55 -0
- package/dist/core/installers/BaseInstaller.d.ts +73 -0
- package/dist/core/installers/BaseInstaller.js +247 -0
- package/dist/core/installers/ClientInstaller.d.ts +17 -0
- package/dist/core/installers/ClientInstaller.js +307 -0
- package/dist/core/installers/CommandInstaller.d.ts +36 -0
- package/dist/core/installers/CommandInstaller.js +170 -0
- package/dist/core/installers/GeneralInstaller.d.ts +32 -0
- package/dist/core/installers/GeneralInstaller.js +87 -0
- package/dist/core/installers/InstallerFactory.d.ts +52 -0
- package/dist/core/installers/InstallerFactory.js +95 -0
- package/dist/core/installers/NpmInstaller.d.ts +25 -0
- package/dist/core/installers/NpmInstaller.js +123 -0
- package/dist/core/installers/PipInstaller.d.ts +25 -0
- package/dist/core/installers/PipInstaller.js +114 -0
- package/dist/core/installers/RequirementInstaller.d.ts +32 -0
- package/dist/core/installers/RequirementInstaller.js +3 -0
- package/dist/core/installers/index.d.ts +6 -0
- package/dist/core/installers/index.js +7 -0
- package/dist/core/types.d.ts +152 -0
- package/dist/core/types.js +16 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +19 -0
- package/dist/services/InstallRequestValidator.d.ts +21 -0
- package/dist/services/InstallRequestValidator.js +99 -0
- package/dist/services/ServerService.d.ts +47 -0
- package/dist/services/ServerService.js +145 -0
- package/dist/utils/UpdateCheckTracker.d.ts +39 -0
- package/dist/utils/UpdateCheckTracker.js +80 -0
- package/dist/utils/clientUtils.d.ts +29 -0
- package/dist/utils/clientUtils.js +105 -0
- package/dist/utils/feedUtils.d.ts +5 -0
- package/dist/utils/feedUtils.js +29 -0
- package/dist/utils/githubAuth.d.ts +1 -0
- package/dist/utils/githubAuth.js +123 -0
- package/dist/utils/logger.d.ts +14 -0
- package/dist/utils/logger.js +90 -0
- package/dist/utils/osUtils.d.ts +16 -0
- package/dist/utils/osUtils.js +235 -0
- package/dist/web/public/css/modal.css +250 -0
- package/dist/web/public/css/notifications.css +70 -0
- package/dist/web/public/index.html +157 -0
- package/dist/web/public/js/api.js +213 -0
- package/dist/web/public/js/modal.js +572 -0
- package/dist/web/public/js/notifications.js +99 -0
- package/dist/web/public/js/serverCategoryDetails.js +210 -0
- package/dist/web/public/js/serverCategoryList.js +82 -0
- package/dist/web/public/modal.html +61 -0
- package/dist/web/public/styles.css +155 -0
- package/dist/web/server.d.ts +5 -0
- package/dist/web/server.js +150 -0
- package/package.json +53 -0
- package/src/cli/commands/install.ts +140 -0
- package/src/cli/commands/list.ts +112 -0
- package/src/cli/commands/pull.ts +16 -0
- package/src/cli/commands/serve.ts +37 -0
- package/src/cli/commands/uninstall.ts +54 -0
- package/src/cli/index.ts +127 -0
- package/src/core/ConfigurationProvider.ts +489 -0
- package/src/core/InstallationService.ts +173 -0
- package/src/core/MCPManager.ts +134 -0
- package/src/core/RequirementService.ts +147 -0
- package/src/core/constants.ts +61 -0
- package/src/core/installers/BaseInstaller.ts +292 -0
- package/src/core/installers/ClientInstaller.ts +423 -0
- package/src/core/installers/CommandInstaller.ts +185 -0
- package/src/core/installers/GeneralInstaller.ts +89 -0
- package/src/core/installers/InstallerFactory.ts +109 -0
- package/src/core/installers/NpmInstaller.ts +128 -0
- package/src/core/installers/PipInstaller.ts +121 -0
- package/src/core/installers/RequirementInstaller.ts +38 -0
- package/src/core/installers/index.ts +9 -0
- package/src/core/types.ts +163 -0
- package/src/index.ts +44 -0
- package/src/services/InstallRequestValidator.ts +112 -0
- package/src/services/ServerService.ts +181 -0
- package/src/utils/UpdateCheckTracker.ts +86 -0
- package/src/utils/clientUtils.ts +112 -0
- package/src/utils/feedUtils.ts +31 -0
- package/src/utils/githubAuth.ts +142 -0
- package/src/utils/logger.ts +101 -0
- package/src/utils/osUtils.ts +250 -0
- package/src/web/public/css/modal.css +250 -0
- package/src/web/public/css/notifications.css +70 -0
- package/src/web/public/index.html +157 -0
- package/src/web/public/js/api.js +213 -0
- package/src/web/public/js/modal.js +572 -0
- package/src/web/public/js/notifications.js +99 -0
- package/src/web/public/js/serverCategoryDetails.js +210 -0
- package/src/web/public/js/serverCategoryList.js +82 -0
- package/src/web/public/modal.html +61 -0
- package/src/web/public/styles.css +155 -0
- package/src/web/server.ts +195 -0
- 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();
|