imcp 0.0.12 → 0.0.14
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/dist/core/ConfigurationProvider.d.ts +2 -1
- package/dist/core/ConfigurationProvider.js +20 -24
- package/dist/core/InstallationService.d.ts +17 -0
- package/dist/core/InstallationService.js +127 -61
- package/dist/core/MCPManager.d.ts +1 -0
- package/dist/core/MCPManager.js +3 -0
- package/dist/core/RequirementService.d.ts +4 -4
- package/dist/core/RequirementService.js +11 -7
- package/dist/core/ServerSchemaProvider.d.ts +1 -1
- package/dist/core/ServerSchemaProvider.js +15 -10
- package/dist/core/constants.d.ts +3 -0
- package/dist/core/constants.js +4 -1
- package/dist/core/installers/clients/ClientInstaller.js +58 -40
- package/dist/core/installers/requirements/PipInstaller.js +10 -5
- package/dist/core/onboard/FeedOnboardService.d.ts +35 -0
- package/dist/core/onboard/FeedOnboardService.js +137 -0
- package/dist/core/types.d.ts +6 -1
- package/dist/core/validators/FeedValidator.d.ts +13 -0
- package/dist/core/validators/FeedValidator.js +27 -0
- package/dist/services/ServerService.d.ts +5 -0
- package/dist/services/ServerService.js +15 -0
- package/dist/utils/githubAuth.js +0 -10
- package/dist/utils/githubUtils.d.ts +16 -0
- package/dist/utils/githubUtils.js +55 -39
- package/dist/web/contract/serverContract.d.ts +64 -0
- package/dist/web/contract/serverContract.js +2 -0
- package/dist/web/public/css/detailsWidget.css +157 -32
- package/dist/web/public/css/onboard.css +44 -0
- package/dist/web/public/css/serverDetails.css +35 -19
- package/dist/web/public/index.html +16 -10
- package/dist/web/public/js/detailsWidget.js +43 -40
- package/dist/web/public/js/modal/index.js +58 -0
- package/dist/web/public/js/modal/installHandler.js +227 -0
- package/dist/web/public/js/modal/installModal.js +163 -0
- package/dist/web/public/js/modal/installation.js +281 -0
- package/dist/web/public/js/modal/loadingModal.js +52 -0
- package/dist/web/public/js/modal/loadingUI.js +74 -0
- package/dist/web/public/js/modal/messageQueue.js +112 -0
- package/dist/web/public/js/modal/modalSetup.js +512 -0
- package/dist/web/public/js/modal/modalUI.js +214 -0
- package/dist/web/public/js/modal/modalUtils.js +49 -0
- package/dist/web/public/js/modal/version.js +20 -0
- package/dist/web/public/js/modal/versionUtils.js +20 -0
- package/dist/web/public/js/modal.js +25 -1041
- package/dist/web/public/js/onboard/formProcessor.js +309 -0
- package/dist/web/public/js/onboard/index.js +131 -0
- package/dist/web/public/js/onboard/state.js +32 -0
- package/dist/web/public/js/onboard/templates.js +375 -0
- package/dist/web/public/js/onboard/uiHandlers.js +196 -0
- package/dist/web/public/js/serverCategoryDetails.js +211 -123
- package/dist/web/public/onboard.html +150 -0
- package/dist/web/server.js +25 -0
- package/package.json +3 -4
- package/src/core/ConfigurationProvider.ts +37 -29
- package/src/core/InstallationService.ts +176 -62
- package/src/core/MCPManager.ts +4 -0
- package/src/core/RequirementService.ts +12 -8
- package/src/core/ServerSchemaLoader.ts +48 -0
- package/src/core/ServerSchemaProvider.ts +137 -0
- package/src/core/constants.ts +4 -1
- package/src/core/installers/clients/ClientInstaller.ts +66 -49
- package/src/core/installers/requirements/PipInstaller.ts +10 -5
- package/src/core/types.ts +6 -1
- package/src/services/ServerService.ts +15 -0
- package/src/utils/githubAuth.ts +14 -27
- package/src/utils/githubUtils.ts +84 -47
- package/src/web/public/css/detailsWidget.css +235 -0
- package/src/web/public/css/serverDetails.css +126 -0
- package/src/web/public/index.html +16 -10
- package/src/web/public/js/detailsWidget.js +264 -0
- package/src/web/public/js/modal/index.js +58 -0
- package/src/web/public/js/modal/installModal.js +163 -0
- package/src/web/public/js/modal/installation.js +281 -0
- package/src/web/public/js/modal/loadingModal.js +52 -0
- package/src/web/public/js/modal/messageQueue.js +112 -0
- package/src/web/public/js/modal/modalSetup.js +512 -0
- package/src/web/public/js/modal/modalUtils.js +49 -0
- package/src/web/public/js/modal/versionUtils.js +20 -0
- package/src/web/public/js/modal.js +25 -1041
- package/src/web/public/js/serverCategoryDetails.js +211 -123
- package/src/web/server.ts +31 -0
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
RequirementStatus,
|
|
15
15
|
MCPServerStatus,
|
|
16
16
|
OperationStatus,
|
|
17
|
-
ClientSettings
|
|
17
|
+
ClientSettings,
|
|
18
|
+
McpConfig
|
|
18
19
|
} from './types.js';
|
|
19
20
|
import { ConfigurationLoader } from './ConfigurationLoader.js';
|
|
20
21
|
|
|
@@ -115,6 +116,12 @@ export class ConfigurationProvider {
|
|
|
115
116
|
});
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
async getServerMcpConfig(categoryName: string, serverName: string): Promise<McpConfig | undefined> {
|
|
120
|
+
return await this.withLock(async () => {
|
|
121
|
+
return this.configuration.feeds[categoryName]?.mcpServers.find(s => s.name === serverName);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
118
125
|
async getInstallationStatus(categoryName: string): Promise<InstallationStatus | undefined> {
|
|
119
126
|
return await this.withLock(async () => {
|
|
120
127
|
// Inline getServerCategory logic
|
|
@@ -300,35 +307,36 @@ export class ConfigurationProvider {
|
|
|
300
307
|
await fs.mkdir(LOCAL_FEEDS_DIR, { recursive: true });
|
|
301
308
|
await fs.mkdir(this.tempDir, { recursive: true });
|
|
302
309
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
310
|
+
// Clean up temp directory
|
|
311
|
+
await fs.rm(this.tempDir, { recursive: true, force: true });
|
|
312
|
+
|
|
313
|
+
// Download latest release
|
|
314
|
+
Logger.debug('Downloading latest release...');
|
|
315
|
+
const { downloadGithubRelease } = await import('../utils/githubUtils.js');
|
|
316
|
+
const { version, downloadPath } = await downloadGithubRelease(
|
|
317
|
+
GITHUB_REPO.repoName,
|
|
318
|
+
'latest',
|
|
319
|
+
GITHUB_REPO.feedAssetsName,
|
|
320
|
+
undefined,
|
|
321
|
+
true,
|
|
322
|
+
this.tempDir
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
Logger.debug({
|
|
326
|
+
action: 'download_release',
|
|
327
|
+
downloadPath,
|
|
328
|
+
version,
|
|
329
|
+
repoName: GITHUB_REPO.repoName,
|
|
330
|
+
});
|
|
323
331
|
|
|
324
332
|
Logger.debug('Updating local feeds...');
|
|
325
333
|
await fs.rm(LOCAL_FEEDS_DIR, { recursive: true, force: true });
|
|
326
|
-
const sourceFeedsDir =
|
|
334
|
+
const sourceFeedsDir = downloadPath;
|
|
327
335
|
|
|
328
336
|
try {
|
|
329
|
-
await fs.access(
|
|
337
|
+
await fs.access(downloadPath);
|
|
330
338
|
} catch (err) {
|
|
331
|
-
throw new Error(`Could not find feeds directory in
|
|
339
|
+
throw new Error(`Could not find feeds directory in downloaded path: ${sourceFeedsDir}`);
|
|
332
340
|
}
|
|
333
341
|
|
|
334
342
|
await fs.cp(sourceFeedsDir, LOCAL_FEEDS_DIR, { recursive: true, force: true });
|
|
@@ -362,7 +370,7 @@ export class ConfigurationProvider {
|
|
|
362
370
|
return await this.withLock(async () => {
|
|
363
371
|
// Load utils in async context to avoid circular dependencies
|
|
364
372
|
const { readJsonFile, writeJsonFile } = await import('../utils/clientUtils.js');
|
|
365
|
-
|
|
373
|
+
|
|
366
374
|
// Filter clients if target is specified
|
|
367
375
|
const clientEntries = Object.entries(SUPPORTED_CLIENTS as Record<string, ClientSettings>);
|
|
368
376
|
const targetClients = target
|
|
@@ -373,11 +381,11 @@ export class ConfigurationProvider {
|
|
|
373
381
|
const settingPath = process.env.CODE_INSIDERS
|
|
374
382
|
? clientSettings.codeInsiderSettingPath
|
|
375
383
|
: clientSettings.codeSettingPath;
|
|
376
|
-
|
|
384
|
+
|
|
377
385
|
try {
|
|
378
386
|
const content = await readJsonFile(settingPath, true);
|
|
379
387
|
let modified = false;
|
|
380
|
-
|
|
388
|
+
|
|
381
389
|
// Handle GitHub Copilot's different structure
|
|
382
390
|
if (clientName === 'GithubCopilot' && content.mcp?.servers?.[serverName]) {
|
|
383
391
|
delete content.mcp.servers[serverName];
|
|
@@ -386,7 +394,7 @@ export class ConfigurationProvider {
|
|
|
386
394
|
delete content.mcpServers[serverName];
|
|
387
395
|
modified = true;
|
|
388
396
|
}
|
|
389
|
-
|
|
397
|
+
|
|
390
398
|
// Only write if we actually modified the content
|
|
391
399
|
if (modified) {
|
|
392
400
|
await writeJsonFile(settingPath, content);
|
|
@@ -396,7 +404,7 @@ export class ConfigurationProvider {
|
|
|
396
404
|
Logger.error(`Failed to remove server ${serverName} from client ${clientName} settings:`, error);
|
|
397
405
|
}
|
|
398
406
|
}
|
|
399
|
-
|
|
407
|
+
|
|
400
408
|
// Also update our in-memory configuration
|
|
401
409
|
if (this.configuration.clientMCPSettings) {
|
|
402
410
|
if (target) {
|
|
@@ -50,7 +50,7 @@ export class InstallationService {
|
|
|
50
50
|
// Fire off requirement updates in the background without awaiting completion
|
|
51
51
|
if (options.requirements && options.requirements.length > 0) {
|
|
52
52
|
// Start the process but don't await it - it will run in the background
|
|
53
|
-
this.processRequirementUpdates(categoryName, serverName, options
|
|
53
|
+
this.processRequirementUpdates(categoryName, serverName, options)
|
|
54
54
|
.catch(error => {
|
|
55
55
|
console.error(`Error in background requirement updates: ${error instanceof Error ? error.message : String(error)}`);
|
|
56
56
|
});
|
|
@@ -91,7 +91,7 @@ export class InstallationService {
|
|
|
91
91
|
* @param serverName The server name
|
|
92
92
|
* @param requirements The requirements to update
|
|
93
93
|
*/
|
|
94
|
-
private async processRequirementUpdates(categoryName: string, serverName: string,
|
|
94
|
+
private async processRequirementUpdates(categoryName: string, serverName: string, options: ServerInstallOptions): Promise<void> {
|
|
95
95
|
// Use UpdateCheckTracker to prevent concurrent updates
|
|
96
96
|
const updateCheckTracker = await import('../utils/UpdateCheckTracker.js').then(m => m.updateCheckTracker);
|
|
97
97
|
const operationKey = `requirement-updates-${categoryName}-${serverName}`;
|
|
@@ -116,7 +116,7 @@ export class InstallationService {
|
|
|
116
116
|
const { requirementService } = await import('./RequirementService.js');
|
|
117
117
|
|
|
118
118
|
// Create an array of promises to update all requirements in parallel
|
|
119
|
-
const updatePromises = requirements
|
|
119
|
+
const updatePromises = options.requirements?.map(async (reqToUpdate) => {
|
|
120
120
|
try {
|
|
121
121
|
// Find the full requirement config
|
|
122
122
|
const reqConfig = feedConfig.requirements?.find((r: RequirementConfig) => r.name === reqToUpdate.name);
|
|
@@ -155,8 +155,16 @@ export class InstallationService {
|
|
|
155
155
|
version: reqToUpdate.version
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
-
//
|
|
159
|
-
|
|
158
|
+
// For pip requirements, check if we have a stored pythonEnv
|
|
159
|
+
if (updatedReqConfig.type === 'pip' && currentStatus.pythonEnv && !options?.settings?.pythonEnv) {
|
|
160
|
+
options = {
|
|
161
|
+
...options,
|
|
162
|
+
settings: { ...options?.settings, pythonEnv: currentStatus.pythonEnv }
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Update the requirement with options for pip environment
|
|
167
|
+
const updatedStatus = await requirementService.updateRequirement(updatedReqConfig, reqToUpdate.version, options);
|
|
160
168
|
|
|
161
169
|
// Update requirement status
|
|
162
170
|
await configProvider.updateRequirementStatus(categoryName, reqToUpdate.name, {
|
|
@@ -197,8 +205,10 @@ export class InstallationService {
|
|
|
197
205
|
}
|
|
198
206
|
});
|
|
199
207
|
|
|
200
|
-
// Wait for all updates to complete in parallel
|
|
201
|
-
|
|
208
|
+
// Wait for all updates to complete in parallel if there are any
|
|
209
|
+
if (updatePromises) {
|
|
210
|
+
await Promise.all(updatePromises);
|
|
211
|
+
}
|
|
202
212
|
} finally {
|
|
203
213
|
// Always release the lock when done, even if there was an error
|
|
204
214
|
await updateCheckTracker.endOperation(operationKey);
|
|
@@ -270,67 +280,171 @@ export class InstallationService {
|
|
|
270
280
|
return orderA - orderB;
|
|
271
281
|
});
|
|
272
282
|
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
name: requirement.name,
|
|
279
|
-
version: requirement.version,
|
|
280
|
-
type: 'npm'
|
|
281
|
-
};
|
|
283
|
+
// Start requirements installation in background
|
|
284
|
+
this.installRequirementsInBackground(categoryName, sortedRequirements, options)
|
|
285
|
+
.catch(error => {
|
|
286
|
+
Logger.error(`Error in background requirement installations: ${error instanceof Error ? error.message : String(error)}`);
|
|
287
|
+
});
|
|
282
288
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
name: requirement.name,
|
|
287
|
-
type: requirementConfig.type,
|
|
288
|
-
installed: false,
|
|
289
|
-
error: `No installer found for requirement type: ${requirementConfig.type}`,
|
|
290
|
-
operationStatus: {
|
|
291
|
-
status: 'failed',
|
|
292
|
-
type: 'install',
|
|
293
|
-
target: 'requirement',
|
|
294
|
-
message: `No installer found for requirement type: ${requirementConfig.type}`,
|
|
295
|
-
operationId: this.generateOperationId()
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
289
|
+
// Return immediately while installation continues in background
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
300
292
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Installs requirements in background without blocking the main thread
|
|
295
|
+
* Requirements with the same order are installed in parallel
|
|
296
|
+
*/
|
|
297
|
+
private async installRequirementsInBackground(
|
|
298
|
+
categoryName: string,
|
|
299
|
+
sortedRequirements: Array<{ name: string; version: string; order?: number }>,
|
|
300
|
+
options: ServerInstallOptions
|
|
301
|
+
): Promise<void> {
|
|
302
|
+
const configProvider = ConfigurationProvider.getInstance();
|
|
308
303
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
304
|
+
// Group requirements by order
|
|
305
|
+
type RequirementType = { name: string; version: string; order?: number };
|
|
306
|
+
const requirementGroups = sortedRequirements.reduce<Record<number, RequirementType[]>>((groups, req) => {
|
|
307
|
+
const order = req.order ?? Infinity;
|
|
308
|
+
if (!groups[order]) {
|
|
309
|
+
groups[order] = [];
|
|
310
|
+
}
|
|
311
|
+
groups[order].push(req);
|
|
312
|
+
return groups;
|
|
313
|
+
}, {});
|
|
316
314
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
315
|
+
// Process each group in sequence, but requirements within group in parallel
|
|
316
|
+
const orderKeys = Object.keys(requirementGroups).map(Number).sort((a, b) => a - b);
|
|
317
|
+
for (const order of orderKeys) {
|
|
318
|
+
const group = requirementGroups[order];
|
|
319
|
+
|
|
320
|
+
await Promise.all(group.map(async requirement => {
|
|
321
|
+
try {
|
|
322
|
+
const feeds = await configProvider.getFeedConfiguration(categoryName);
|
|
323
|
+
const requirementConfig = feeds?.requirements?.find((r: RequirementConfig) => r.name === requirement.name) || {
|
|
324
|
+
name: requirement.name,
|
|
325
|
+
version: requirement.version,
|
|
326
|
+
type: 'npm'
|
|
327
327
|
};
|
|
328
|
-
await configProvider.updateRequirementStatus(categoryName, requirement.name, status);
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
}, Promise.resolve());
|
|
332
328
|
|
|
333
|
-
|
|
329
|
+
// For pip requirements, check if we need to use stored pythonEnv
|
|
330
|
+
const currentStatus = await configProvider.getRequirementStatus(categoryName, requirement.name);
|
|
331
|
+
if (requirementConfig.type === 'pip' && currentStatus?.pythonEnv && !options?.settings?.pythonEnv) {
|
|
332
|
+
options = {
|
|
333
|
+
...options,
|
|
334
|
+
settings: { ...options?.settings, pythonEnv: currentStatus.pythonEnv }
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const installer = this.installerFactory.getInstaller(requirementConfig);
|
|
339
|
+
if (!installer) {
|
|
340
|
+
await this.updateRequirementFailureStatus(
|
|
341
|
+
categoryName,
|
|
342
|
+
requirement.name,
|
|
343
|
+
requirementConfig.type,
|
|
344
|
+
`No installer found for requirement type: ${requirementConfig.type}`
|
|
345
|
+
);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const operationId = this.generateOperationId();
|
|
350
|
+
await this.updateRequirementProgressStatus(
|
|
351
|
+
categoryName,
|
|
352
|
+
requirement.name,
|
|
353
|
+
requirementConfig.type,
|
|
354
|
+
operationId
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const installStatus = await installer.install(requirementConfig, options);
|
|
358
|
+
await this.updateRequirementCompletionStatus(
|
|
359
|
+
categoryName,
|
|
360
|
+
requirement.name,
|
|
361
|
+
installStatus,
|
|
362
|
+
operationId
|
|
363
|
+
);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
await this.updateRequirementFailureStatus(
|
|
366
|
+
categoryName,
|
|
367
|
+
requirement.name,
|
|
368
|
+
'unknown',
|
|
369
|
+
error instanceof Error ? error.message : String(error)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Helper to update requirement status for failure case
|
|
378
|
+
*/
|
|
379
|
+
private async updateRequirementFailureStatus(
|
|
380
|
+
categoryName: string,
|
|
381
|
+
requirementName: string,
|
|
382
|
+
requirementType: string,
|
|
383
|
+
errorMessage: string
|
|
384
|
+
): Promise<void> {
|
|
385
|
+
const configProvider = ConfigurationProvider.getInstance();
|
|
386
|
+
await configProvider.updateRequirementStatus(categoryName, requirementName, {
|
|
387
|
+
name: requirementName,
|
|
388
|
+
type: requirementType,
|
|
389
|
+
installed: false,
|
|
390
|
+
error: errorMessage,
|
|
391
|
+
operationStatus: {
|
|
392
|
+
status: 'failed',
|
|
393
|
+
type: 'install',
|
|
394
|
+
target: 'requirement',
|
|
395
|
+
message: `Error installing requirement: ${errorMessage}`,
|
|
396
|
+
operationId: this.generateOperationId()
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Helper to update requirement status for in-progress case
|
|
403
|
+
*/
|
|
404
|
+
private async updateRequirementProgressStatus(
|
|
405
|
+
categoryName: string,
|
|
406
|
+
requirementName: string,
|
|
407
|
+
requirementType: string,
|
|
408
|
+
operationId: string
|
|
409
|
+
): Promise<void> {
|
|
410
|
+
const configProvider = ConfigurationProvider.getInstance();
|
|
411
|
+
await configProvider.updateRequirementStatus(categoryName, requirementName, {
|
|
412
|
+
name: requirementName,
|
|
413
|
+
type: requirementType,
|
|
414
|
+
installed: false,
|
|
415
|
+
inProgress: true,
|
|
416
|
+
operationStatus: {
|
|
417
|
+
status: 'in-progress',
|
|
418
|
+
type: 'install',
|
|
419
|
+
target: 'requirement',
|
|
420
|
+
message: `Installing requirement: ${requirementName}`,
|
|
421
|
+
operationId
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Helper to update requirement status for completion case
|
|
428
|
+
*/
|
|
429
|
+
private async updateRequirementCompletionStatus(
|
|
430
|
+
categoryName: string,
|
|
431
|
+
requirementName: string,
|
|
432
|
+
installStatus: RequirementStatus,
|
|
433
|
+
operationId: string
|
|
434
|
+
): Promise<void> {
|
|
435
|
+
const configProvider = ConfigurationProvider.getInstance();
|
|
436
|
+
await configProvider.updateRequirementStatus(categoryName, requirementName, {
|
|
437
|
+
...installStatus,
|
|
438
|
+
operationStatus: {
|
|
439
|
+
status: installStatus.installed ? 'completed' : 'failed',
|
|
440
|
+
type: 'install',
|
|
441
|
+
target: 'requirement',
|
|
442
|
+
message: installStatus.installed
|
|
443
|
+
? `Requirement ${requirementName} installed successfully`
|
|
444
|
+
: `Failed to install ${requirementName}`,
|
|
445
|
+
operationId
|
|
446
|
+
}
|
|
447
|
+
});
|
|
334
448
|
}
|
|
335
449
|
}
|
|
336
450
|
|
package/src/core/MCPManager.ts
CHANGED
|
@@ -48,6 +48,10 @@ export class MCPManager extends EventEmitter {
|
|
|
48
48
|
return this.configProvider.getFeedConfiguration(categoryName);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
async getServerMcpConfig(categoryName: string, serverName: string) {
|
|
52
|
+
return this.configProvider.getServerMcpConfig(categoryName, serverName);
|
|
53
|
+
}
|
|
54
|
+
|
|
51
55
|
async installServer(
|
|
52
56
|
categoryName: string,
|
|
53
57
|
serverName: string,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RequirementConfig, RequirementStatus } from './types.js';
|
|
1
|
+
import { RequirementConfig, RequirementStatus, ServerInstallOptions } from './types.js';
|
|
2
2
|
import { createInstallerFactory } from './installers/index.js';
|
|
3
3
|
import { exec } from 'child_process';
|
|
4
4
|
import util from 'util';
|
|
@@ -28,12 +28,12 @@ export class RequirementService {
|
|
|
28
28
|
* @param requirement The requirement to install
|
|
29
29
|
* @returns The installation status
|
|
30
30
|
*/
|
|
31
|
-
public async installRequirement(requirement: RequirementConfig): Promise<RequirementStatus> {
|
|
31
|
+
public async installRequirement(requirement: RequirementConfig, options?: ServerInstallOptions): Promise<RequirementStatus> {
|
|
32
32
|
// Validate requirement
|
|
33
33
|
this.validateRequirement(requirement);
|
|
34
34
|
|
|
35
35
|
// Install the requirement
|
|
36
|
-
return await this.installerFactory.install(requirement);
|
|
36
|
+
return await this.installerFactory.install(requirement, options);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -41,12 +41,12 @@ export class RequirementService {
|
|
|
41
41
|
* @param requirement The requirement to check
|
|
42
42
|
* @returns The installation status
|
|
43
43
|
*/
|
|
44
|
-
public async checkRequirementStatus(requirement: RequirementConfig): Promise<RequirementStatus> {
|
|
44
|
+
public async checkRequirementStatus(requirement: RequirementConfig, options?: ServerInstallOptions): Promise<RequirementStatus> {
|
|
45
45
|
// Validate requirement
|
|
46
46
|
this.validateRequirement(requirement);
|
|
47
47
|
|
|
48
48
|
// Check the installation status
|
|
49
|
-
return await this.installerFactory.checkInstallation(requirement);
|
|
49
|
+
return await this.installerFactory.checkInstallation(requirement, options);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
@@ -66,7 +66,11 @@ export class RequirementService {
|
|
|
66
66
|
return currentStatus;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
// Pass pythonEnv from currentStatus if it exists for pip packages
|
|
70
|
+
const options = requirement.type === 'pip' && currentStatus.pythonEnv
|
|
71
|
+
? { settings: { pythonEnv: currentStatus.pythonEnv } }
|
|
72
|
+
: undefined;
|
|
73
|
+
const status = await this.checkRequirementStatus(requirement, options);
|
|
70
74
|
return await (installer as any).checkForUpdates(requirement, status);
|
|
71
75
|
}
|
|
72
76
|
|
|
@@ -76,7 +80,7 @@ export class RequirementService {
|
|
|
76
80
|
* @param updateVersion The version to update to
|
|
77
81
|
* @returns The updated requirement status
|
|
78
82
|
*/
|
|
79
|
-
public async updateRequirement(requirement: RequirementConfig, updateVersion: string): Promise<RequirementStatus> {
|
|
83
|
+
public async updateRequirement(requirement: RequirementConfig, updateVersion: string, options?: ServerInstallOptions): Promise<RequirementStatus> {
|
|
80
84
|
// Validate requirement
|
|
81
85
|
this.validateRequirement(requirement);
|
|
82
86
|
|
|
@@ -87,7 +91,7 @@ export class RequirementService {
|
|
|
87
91
|
};
|
|
88
92
|
|
|
89
93
|
// Install the updated version
|
|
90
|
-
return await this.
|
|
94
|
+
return await this.installerFactory.install(updatedRequirement, options);
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
/**
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { LOCAL_FEEDS_SCHEMA_DIR } from './constants.js';
|
|
4
|
+
import { Logger } from '../utils/logger.js';
|
|
5
|
+
import { ServerSchema } from './ServerSchemaProvider.js';
|
|
6
|
+
|
|
7
|
+
export class ServerSchemaLoader {
|
|
8
|
+
/**
|
|
9
|
+
* Load schema for a specific server in a category
|
|
10
|
+
*/
|
|
11
|
+
static async loadSchema(categoryName: string, serverName: string): Promise<ServerSchema | undefined> {
|
|
12
|
+
try {
|
|
13
|
+
const schemaPath = path.join(LOCAL_FEEDS_SCHEMA_DIR, categoryName, `${serverName}.json`);
|
|
14
|
+
const content = await fs.readFile(schemaPath, 'utf8');
|
|
15
|
+
const schema = JSON.parse(content);
|
|
16
|
+
|
|
17
|
+
// Validate schema structure
|
|
18
|
+
if (!schema.version || !schema.schema) {
|
|
19
|
+
Logger.debug(`Invalid schema format for server ${serverName} in category ${categoryName}`);
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
schema: schema
|
|
25
|
+
};
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
28
|
+
Logger.debug(`No schema file found for server ${serverName} in category ${categoryName}`);
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
Logger.error(`Error loading schema for server ${serverName} in category ${categoryName}:`, error);
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate schema content against expected format
|
|
38
|
+
*/
|
|
39
|
+
static validateSchema(schema: any): boolean {
|
|
40
|
+
return (
|
|
41
|
+
typeof schema === 'object' &&
|
|
42
|
+
schema !== null &&
|
|
43
|
+
typeof schema.version === 'string' &&
|
|
44
|
+
typeof schema.schema === 'object' &&
|
|
45
|
+
schema.schema !== null
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { LOCAL_FEEDS_SCHEMA_DIR } from './constants.js';
|
|
4
|
+
import { Logger } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
export interface ServerSchema {
|
|
7
|
+
schema: Record<string, any>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ServerSchemaProvider {
|
|
11
|
+
private static instance: ServerSchemaProvider;
|
|
12
|
+
private schemaMap: Map<string, Map<string, ServerSchema>>;
|
|
13
|
+
private schemaLock: Promise<void> = Promise.resolve();
|
|
14
|
+
|
|
15
|
+
private constructor() {
|
|
16
|
+
this.schemaMap = new Map();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public static async getInstance(): Promise<ServerSchemaProvider> {
|
|
20
|
+
if (!ServerSchemaProvider.instance) {
|
|
21
|
+
ServerSchemaProvider.instance = new ServerSchemaProvider();
|
|
22
|
+
await ServerSchemaProvider.instance.initialize();
|
|
23
|
+
}
|
|
24
|
+
return ServerSchemaProvider.instance;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private async withLock<T>(operation: () => Promise<T>): Promise<T> {
|
|
28
|
+
const current = this.schemaLock;
|
|
29
|
+
let resolve: () => void;
|
|
30
|
+
this.schemaLock = new Promise<void>(r => resolve = r);
|
|
31
|
+
try {
|
|
32
|
+
await current;
|
|
33
|
+
return await operation();
|
|
34
|
+
} finally {
|
|
35
|
+
resolve!();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async initialize(): Promise<void> {
|
|
40
|
+
await this.withLock(async () => {
|
|
41
|
+
try {
|
|
42
|
+
// Create feeds directory if it doesn't exist
|
|
43
|
+
await fs.mkdir(LOCAL_FEEDS_SCHEMA_DIR, { recursive: true });
|
|
44
|
+
|
|
45
|
+
// Load all schemas from the feeds directory
|
|
46
|
+
await this.loadAllSchemas();
|
|
47
|
+
} catch (error) {
|
|
48
|
+
Logger.error('Error during schema initialization:', error);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async loadSchema(categoryName: string, schemaFileName: string): Promise<ServerSchema | undefined> {
|
|
55
|
+
try {
|
|
56
|
+
const schemaPath = path.join(LOCAL_FEEDS_SCHEMA_DIR, categoryName, schemaFileName);
|
|
57
|
+
const content = await fs.readFile(schemaPath, 'utf8');
|
|
58
|
+
const schema = JSON.parse(content);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
schema: schema
|
|
62
|
+
};
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
65
|
+
Logger.debug(`No schema file found for ${schemaFileName} in category ${categoryName}`);
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
Logger.error(`Error loading schema ${schemaFileName} in category ${categoryName}:`, error);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async loadAllSchemas(): Promise<void> {
|
|
74
|
+
this.schemaMap.clear();
|
|
75
|
+
|
|
76
|
+
// Read server category directories
|
|
77
|
+
const categoryDirs = await fs.readdir(LOCAL_FEEDS_SCHEMA_DIR, { withFileTypes: true });
|
|
78
|
+
|
|
79
|
+
for (const categoryDir of categoryDirs) {
|
|
80
|
+
if (categoryDir.isDirectory()) {
|
|
81
|
+
const categoryPath = path.join(LOCAL_FEEDS_SCHEMA_DIR, categoryDir.name);
|
|
82
|
+
const serverFiles = await fs.readdir(categoryPath);
|
|
83
|
+
|
|
84
|
+
const serverSchemas = new Map<string, ServerSchema>();
|
|
85
|
+
|
|
86
|
+
for (const file of serverFiles) {
|
|
87
|
+
if (file.endsWith('.json')) {
|
|
88
|
+
try {
|
|
89
|
+
const schema = await this.loadSchema(categoryDir.name, file);
|
|
90
|
+
if (schema) {
|
|
91
|
+
// Store with the complete file name for direct lookup
|
|
92
|
+
serverSchemas.set(file, schema);
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
Logger.error(`Error loading schema for file ${file} in category ${categoryDir.name}:`, error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (serverSchemas.size > 0) {
|
|
101
|
+
this.schemaMap.set(categoryDir.name, serverSchemas);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async getSchema(categoryName: string, schemaFileName: string): Promise<ServerSchema | undefined> {
|
|
108
|
+
return await this.withLock(async () => {
|
|
109
|
+
const categorySchemas = this.schemaMap.get(categoryName);
|
|
110
|
+
if (!categorySchemas) {
|
|
111
|
+
Logger.debug(`No schemas found for category ${categoryName}`);
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const schema = categorySchemas.get(schemaFileName);
|
|
115
|
+
if (!schema) {
|
|
116
|
+
Logger.debug(`Schema ${schemaFileName} not found in category ${categoryName}`);
|
|
117
|
+
}
|
|
118
|
+
return schema;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async reloadSchemas(): Promise<void> {
|
|
123
|
+
return await this.withLock(async () => {
|
|
124
|
+
await this.loadAllSchemas();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Export a lazy initialized singleton instance getter
|
|
130
|
+
let initPromise: Promise<ServerSchemaProvider> | null = null;
|
|
131
|
+
|
|
132
|
+
export function getServerSchemaProvider(): Promise<ServerSchemaProvider> {
|
|
133
|
+
if (!initPromise) {
|
|
134
|
+
initPromise = ServerSchemaProvider.getInstance();
|
|
135
|
+
}
|
|
136
|
+
return initPromise;
|
|
137
|
+
}
|