Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/mcp/common/mcpManagementService.ts
5252 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { RunOnceScheduler } from '../../../base/common/async.js';
7
import { VSBuffer } from '../../../base/common/buffer.js';
8
import { CancellationToken } from '../../../base/common/cancellation.js';
9
import { Emitter, Event } from '../../../base/common/event.js';
10
import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js';
11
import { Disposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
12
import { ResourceMap } from '../../../base/common/map.js';
13
import { equals } from '../../../base/common/objects.js';
14
import { isString } from '../../../base/common/types.js';
15
import { URI } from '../../../base/common/uri.js';
16
import { localize } from '../../../nls.js';
17
import { ConfigurationTarget } from '../../configuration/common/configuration.js';
18
import { IEnvironmentService } from '../../environment/common/environment.js';
19
import { IFileService } from '../../files/common/files.js';
20
import { IInstantiationService } from '../../instantiation/common/instantiation.js';
21
import { ILogService } from '../../log/common/log.js';
22
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
23
import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';
24
import { DidUninstallMcpServerEvent, IGalleryMcpServer, ILocalMcpServer, IMcpGalleryService, IMcpManagementService, IMcpServerInput, IGalleryMcpServerConfiguration, InstallMcpServerEvent, InstallMcpServerResult, RegistryType, UninstallMcpServerEvent, InstallOptions, UninstallOptions, IInstallableMcpServer, IAllowedMcpServersService, IMcpServerArgument, IMcpServerKeyValueInput, McpServerConfigurationParseResult } from './mcpManagement.js';
25
import { IMcpServerVariable, McpServerVariableType, IMcpServerConfiguration, McpServerType } from './mcpPlatformTypes.js';
26
import { IMcpResourceScannerService, McpResourceTarget } from './mcpResourceScannerService.js';
27
28
export interface ILocalMcpServerInfo {
29
name: string;
30
version?: string;
31
displayName?: string;
32
galleryId?: string;
33
galleryUrl?: string;
34
description?: string;
35
repositoryUrl?: string;
36
publisher?: string;
37
publisherDisplayName?: string;
38
icon?: {
39
dark: string;
40
light: string;
41
};
42
codicon?: string;
43
manifest?: IGalleryMcpServerConfiguration;
44
readmeUrl?: URI;
45
location?: URI;
46
licenseUrl?: string;
47
}
48
49
export abstract class AbstractCommonMcpManagementService extends Disposable implements IMcpManagementService {
50
51
_serviceBrand: undefined;
52
53
abstract onInstallMcpServer: Event<InstallMcpServerEvent>;
54
abstract onDidInstallMcpServers: Event<readonly InstallMcpServerResult[]>;
55
abstract onDidUpdateMcpServers: Event<readonly InstallMcpServerResult[]>;
56
abstract onUninstallMcpServer: Event<UninstallMcpServerEvent>;
57
abstract onDidUninstallMcpServer: Event<DidUninstallMcpServerEvent>;
58
59
abstract getInstalled(mcpResource?: URI): Promise<ILocalMcpServer[]>;
60
abstract install(server: IInstallableMcpServer, options?: InstallOptions): Promise<ILocalMcpServer>;
61
abstract installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer>;
62
abstract updateMetadata(local: ILocalMcpServer, server: IGalleryMcpServer, profileLocation?: URI): Promise<ILocalMcpServer>;
63
abstract uninstall(server: ILocalMcpServer, options?: UninstallOptions): Promise<void>;
64
abstract canInstall(server: IGalleryMcpServer | IInstallableMcpServer): true | IMarkdownString;
65
66
constructor(
67
@ILogService protected readonly logService: ILogService
68
) {
69
super();
70
}
71
72
getMcpServerConfigurationFromManifest(manifest: IGalleryMcpServerConfiguration, packageType: RegistryType): McpServerConfigurationParseResult {
73
74
// remote
75
if (packageType === RegistryType.REMOTE && manifest.remotes?.length) {
76
const url = manifest.remotes[0].url;
77
const headers = manifest.remotes[0].headers ?? [];
78
const { inputs, variables } = this.processKeyValueInputs(url.startsWith('https://api.githubcopilot.com/mcp') ? headers.filter(h => h.name.toLowerCase() !== 'authorization') : headers);
79
return {
80
mcpServerConfiguration: {
81
config: {
82
type: McpServerType.REMOTE,
83
url: manifest.remotes[0].url,
84
headers: Object.keys(inputs).length ? inputs : undefined,
85
},
86
inputs: variables.length ? variables : undefined,
87
},
88
notices: [],
89
};
90
}
91
92
// local
93
const serverPackage = manifest.packages?.find(p => p.registryType === packageType) ?? manifest.packages?.[0];
94
if (!serverPackage) {
95
throw new Error(`No server package found`);
96
}
97
98
const args: string[] = [];
99
const inputs: IMcpServerVariable[] = [];
100
const env: Record<string, string> = {};
101
const notices: string[] = [];
102
103
if (serverPackage.registryType === RegistryType.DOCKER) {
104
args.push('run');
105
args.push('-i');
106
args.push('--rm');
107
}
108
109
if (serverPackage.runtimeArguments?.length) {
110
const result = this.processArguments(serverPackage.runtimeArguments ?? []);
111
args.push(...result.args);
112
inputs.push(...result.variables);
113
notices.push(...result.notices);
114
}
115
116
if (serverPackage.environmentVariables?.length) {
117
const { inputs: envInputs, variables: envVariables, notices: envNotices } = this.processKeyValueInputs(serverPackage.environmentVariables ?? []);
118
inputs.push(...envVariables);
119
notices.push(...envNotices);
120
for (const [name, value] of Object.entries(envInputs)) {
121
env[name] = value;
122
if (serverPackage.registryType === RegistryType.DOCKER) {
123
args.push('-e');
124
args.push(name);
125
}
126
}
127
}
128
129
switch (serverPackage.registryType) {
130
case RegistryType.NODE:
131
if (serverPackage.registryBaseUrl) {
132
args.push('--registry', serverPackage.registryBaseUrl);
133
}
134
args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier);
135
break;
136
case RegistryType.PYTHON:
137
if (serverPackage.registryBaseUrl) {
138
args.push('--index-url', serverPackage.registryBaseUrl);
139
}
140
args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier);
141
break;
142
case RegistryType.DOCKER:
143
{
144
const dockerIdentifier = serverPackage.registryBaseUrl
145
? `${serverPackage.registryBaseUrl}/${serverPackage.identifier}`
146
: serverPackage.identifier;
147
args.push(serverPackage.version ? `${dockerIdentifier}:${serverPackage.version}` : dockerIdentifier);
148
break;
149
}
150
case RegistryType.NUGET:
151
args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier);
152
args.push('--yes'); // installation is confirmed by the UI, so --yes is appropriate here
153
if (serverPackage.registryBaseUrl) {
154
args.push('--source', serverPackage.registryBaseUrl);
155
}
156
if (serverPackage.packageArguments?.length) {
157
args.push('--');
158
}
159
break;
160
}
161
162
if (serverPackage.packageArguments?.length) {
163
const result = this.processArguments(serverPackage.packageArguments);
164
args.push(...result.args);
165
inputs.push(...result.variables);
166
notices.push(...result.notices);
167
}
168
169
return {
170
notices,
171
mcpServerConfiguration: {
172
config: {
173
type: McpServerType.LOCAL,
174
command: this.getCommandName(serverPackage.registryType),
175
args: args.length ? args : undefined,
176
env: Object.keys(env).length ? env : undefined,
177
},
178
inputs: inputs.length ? inputs : undefined,
179
}
180
};
181
}
182
183
protected getCommandName(packageType: RegistryType): string {
184
switch (packageType) {
185
case RegistryType.NODE: return 'npx';
186
case RegistryType.DOCKER: return 'docker';
187
case RegistryType.PYTHON: return 'uvx';
188
case RegistryType.NUGET: return 'dnx';
189
}
190
return packageType;
191
}
192
193
protected getVariables(variableInputs: Record<string, IMcpServerInput>): IMcpServerVariable[] {
194
const variables: IMcpServerVariable[] = [];
195
for (const [key, value] of Object.entries(variableInputs)) {
196
variables.push({
197
id: key,
198
type: value.choices ? McpServerVariableType.PICK : McpServerVariableType.PROMPT,
199
description: value.description ?? '',
200
password: !!value.isSecret,
201
default: value.default,
202
options: value.choices,
203
});
204
}
205
return variables;
206
}
207
208
private processKeyValueInputs(keyValueInputs: ReadonlyArray<IMcpServerKeyValueInput>): { inputs: Record<string, string>; variables: IMcpServerVariable[]; notices: string[] } {
209
const notices: string[] = [];
210
const inputs: Record<string, string> = {};
211
const variables: IMcpServerVariable[] = [];
212
213
for (const input of keyValueInputs) {
214
const inputVariables = input.variables ? this.getVariables(input.variables) : [];
215
let value = input.value || '';
216
217
// If explicit variables exist, use them regardless of value
218
if (inputVariables.length) {
219
for (const variable of inputVariables) {
220
value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`);
221
}
222
variables.push(...inputVariables);
223
} else if (!value && (input.description || input.choices || input.default !== undefined)) {
224
// Only create auto-generated input variable if no explicit variables and no value
225
variables.push({
226
id: input.name,
227
type: input.choices ? McpServerVariableType.PICK : McpServerVariableType.PROMPT,
228
description: input.description ?? '',
229
password: !!input.isSecret,
230
default: input.default,
231
options: input.choices,
232
});
233
value = `\${input:${input.name}}`;
234
}
235
236
inputs[input.name] = value;
237
}
238
239
return { inputs, variables, notices };
240
}
241
242
private processArguments(argumentsList: readonly IMcpServerArgument[]): { args: string[]; variables: IMcpServerVariable[]; notices: string[] } {
243
const args: string[] = [];
244
const variables: IMcpServerVariable[] = [];
245
const notices: string[] = [];
246
for (const arg of argumentsList) {
247
const argVariables = arg.variables ? this.getVariables(arg.variables) : [];
248
249
if (arg.type === 'positional') {
250
let value = arg.value;
251
if (value) {
252
for (const variable of argVariables) {
253
value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`);
254
}
255
args.push(value);
256
if (argVariables.length) {
257
variables.push(...argVariables);
258
}
259
} else if (arg.valueHint && (arg.description || arg.default !== undefined)) {
260
// Create input variable for positional argument without value
261
variables.push({
262
id: arg.valueHint,
263
type: McpServerVariableType.PROMPT,
264
description: arg.description ?? '',
265
password: false,
266
default: arg.default,
267
});
268
args.push(`\${input:${arg.valueHint}}`);
269
} else {
270
// Fallback to value_hint as literal
271
args.push(arg.valueHint ?? '');
272
}
273
} else if (arg.type === 'named') {
274
if (!arg.name) {
275
notices.push(`Named argument is missing a name. ${JSON.stringify(arg)}`);
276
continue;
277
}
278
args.push(arg.name);
279
if (arg.value) {
280
let value = arg.value;
281
for (const variable of argVariables) {
282
value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`);
283
}
284
args.push(value);
285
if (argVariables.length) {
286
variables.push(...argVariables);
287
}
288
} else if (arg.description || arg.default !== undefined) {
289
// Create input variable for named argument without value
290
const variableId = arg.name.replace(/^--?/, '');
291
variables.push({
292
id: variableId,
293
type: McpServerVariableType.PROMPT,
294
description: arg.description ?? '',
295
password: false,
296
default: arg.default,
297
});
298
args.push(`\${input:${variableId}}`);
299
}
300
}
301
}
302
return { args, variables, notices };
303
}
304
305
}
306
307
export abstract class AbstractMcpResourceManagementService extends AbstractCommonMcpManagementService {
308
309
private initializePromise: Promise<void> | undefined;
310
private readonly reloadConfigurationScheduler: RunOnceScheduler;
311
private local = new Map<string, ILocalMcpServer>();
312
313
protected readonly _onInstallMcpServer = this._register(new Emitter<InstallMcpServerEvent>());
314
readonly onInstallMcpServer = this._onInstallMcpServer.event;
315
316
protected readonly _onDidInstallMcpServers = this._register(new Emitter<InstallMcpServerResult[]>());
317
get onDidInstallMcpServers() { return this._onDidInstallMcpServers.event; }
318
319
protected readonly _onDidUpdateMcpServers = this._register(new Emitter<InstallMcpServerResult[]>());
320
get onDidUpdateMcpServers() { return this._onDidUpdateMcpServers.event; }
321
322
protected readonly _onUninstallMcpServer = this._register(new Emitter<UninstallMcpServerEvent>());
323
get onUninstallMcpServer() { return this._onUninstallMcpServer.event; }
324
325
protected _onDidUninstallMcpServer = this._register(new Emitter<DidUninstallMcpServerEvent>());
326
get onDidUninstallMcpServer() { return this._onDidUninstallMcpServer.event; }
327
328
constructor(
329
protected readonly mcpResource: URI,
330
protected readonly target: McpResourceTarget,
331
@IMcpGalleryService protected readonly mcpGalleryService: IMcpGalleryService,
332
@IFileService protected readonly fileService: IFileService,
333
@IUriIdentityService protected readonly uriIdentityService: IUriIdentityService,
334
@ILogService logService: ILogService,
335
@IMcpResourceScannerService protected readonly mcpResourceScannerService: IMcpResourceScannerService,
336
) {
337
super(logService);
338
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.updateLocal(), 50));
339
}
340
341
private initialize(): Promise<void> {
342
if (!this.initializePromise) {
343
this.initializePromise = (async () => {
344
try {
345
this.local = await this.populateLocalServers();
346
} finally {
347
this.startWatching();
348
}
349
})();
350
}
351
return this.initializePromise;
352
}
353
354
private async populateLocalServers(): Promise<Map<string, ILocalMcpServer>> {
355
this.logService.trace('AbstractMcpResourceManagementService#populateLocalServers', this.mcpResource.toString());
356
const local = new Map<string, ILocalMcpServer>();
357
try {
358
const scannedMcpServers = await this.mcpResourceScannerService.scanMcpServers(this.mcpResource, this.target);
359
if (scannedMcpServers.servers) {
360
await Promise.allSettled(Object.entries(scannedMcpServers.servers).map(async ([name, scannedServer]) => {
361
const server = await this.scanLocalServer(name, scannedServer);
362
local.set(name, server);
363
}));
364
}
365
} catch (error) {
366
this.logService.debug('Could not read user MCP servers:', error);
367
throw error;
368
}
369
return local;
370
}
371
372
private startWatching(): void {
373
this._register(this.fileService.watch(this.mcpResource));
374
this._register(this.fileService.onDidFilesChange(e => {
375
if (e.affects(this.mcpResource)) {
376
this.reloadConfigurationScheduler.schedule();
377
}
378
}));
379
}
380
381
protected async updateLocal(): Promise<void> {
382
try {
383
const current = await this.populateLocalServers();
384
385
const added: ILocalMcpServer[] = [];
386
const updated: ILocalMcpServer[] = [];
387
const removed = [...this.local.keys()].filter(name => !current.has(name));
388
389
for (const server of removed) {
390
this.local.delete(server);
391
}
392
393
for (const [name, server] of current) {
394
const previous = this.local.get(name);
395
if (previous) {
396
if (!equals(previous, server)) {
397
updated.push(server);
398
this.local.set(name, server);
399
}
400
} else {
401
added.push(server);
402
this.local.set(name, server);
403
}
404
}
405
406
for (const server of removed) {
407
this.local.delete(server);
408
this._onDidUninstallMcpServer.fire({ name: server, mcpResource: this.mcpResource });
409
}
410
411
if (updated.length) {
412
this._onDidUpdateMcpServers.fire(updated.map(server => ({ name: server.name, local: server, mcpResource: this.mcpResource })));
413
}
414
415
if (added.length) {
416
this._onDidInstallMcpServers.fire(added.map(server => ({ name: server.name, local: server, mcpResource: this.mcpResource })));
417
}
418
419
} catch (error) {
420
this.logService.error('Failed to load installed MCP servers:', error);
421
}
422
}
423
424
async getInstalled(): Promise<ILocalMcpServer[]> {
425
await this.initialize();
426
return Array.from(this.local.values());
427
}
428
429
protected async scanLocalServer(name: string, config: IMcpServerConfiguration): Promise<ILocalMcpServer> {
430
let mcpServerInfo = await this.getLocalServerInfo(name, config);
431
if (!mcpServerInfo) {
432
mcpServerInfo = { name, version: config.version, galleryUrl: isString(config.gallery) ? config.gallery : undefined };
433
}
434
435
return {
436
name,
437
config,
438
mcpResource: this.mcpResource,
439
version: mcpServerInfo.version,
440
location: mcpServerInfo.location,
441
displayName: mcpServerInfo.displayName,
442
description: mcpServerInfo.description,
443
publisher: mcpServerInfo.publisher,
444
publisherDisplayName: mcpServerInfo.publisherDisplayName,
445
galleryUrl: mcpServerInfo.galleryUrl,
446
galleryId: mcpServerInfo.galleryId,
447
repositoryUrl: mcpServerInfo.repositoryUrl,
448
readmeUrl: mcpServerInfo.readmeUrl,
449
icon: mcpServerInfo.icon,
450
codicon: mcpServerInfo.codicon,
451
manifest: mcpServerInfo.manifest,
452
source: config.gallery ? 'gallery' : 'local'
453
};
454
}
455
456
async install(server: IInstallableMcpServer, options?: Omit<InstallOptions, 'mcpResource'>): Promise<ILocalMcpServer> {
457
this.logService.trace('MCP Management Service: install', server.name);
458
459
this._onInstallMcpServer.fire({ name: server.name, mcpResource: this.mcpResource });
460
try {
461
await this.mcpResourceScannerService.addMcpServers([server], this.mcpResource, this.target);
462
await this.updateLocal();
463
const local = this.local.get(server.name);
464
if (!local) {
465
throw new Error(`Failed to install MCP server: ${server.name}`);
466
}
467
return local;
468
} catch (e) {
469
this._onDidInstallMcpServers.fire([{ name: server.name, error: e, mcpResource: this.mcpResource }]);
470
throw e;
471
}
472
}
473
474
async uninstall(server: ILocalMcpServer, options?: Omit<UninstallOptions, 'mcpResource'>): Promise<void> {
475
this.logService.trace('MCP Management Service: uninstall', server.name);
476
this._onUninstallMcpServer.fire({ name: server.name, mcpResource: this.mcpResource });
477
478
try {
479
const currentServers = await this.mcpResourceScannerService.scanMcpServers(this.mcpResource, this.target);
480
if (!currentServers.servers) {
481
return;
482
}
483
await this.mcpResourceScannerService.removeMcpServers([server.name], this.mcpResource, this.target);
484
if (server.location) {
485
await this.fileService.del(URI.revive(server.location), { recursive: true });
486
}
487
await this.updateLocal();
488
} catch (e) {
489
this._onDidUninstallMcpServer.fire({ name: server.name, error: e, mcpResource: this.mcpResource });
490
throw e;
491
}
492
}
493
494
protected abstract getLocalServerInfo(name: string, mcpServerConfig: IMcpServerConfiguration): Promise<ILocalMcpServerInfo | undefined>;
495
protected abstract installFromUri(uri: URI, options?: Omit<InstallOptions, 'mcpResource'>): Promise<ILocalMcpServer>;
496
}
497
498
export class McpUserResourceManagementService extends AbstractMcpResourceManagementService {
499
500
protected readonly mcpLocation: URI;
501
502
constructor(
503
mcpResource: URI,
504
@IMcpGalleryService mcpGalleryService: IMcpGalleryService,
505
@IFileService fileService: IFileService,
506
@IUriIdentityService uriIdentityService: IUriIdentityService,
507
@ILogService logService: ILogService,
508
@IMcpResourceScannerService mcpResourceScannerService: IMcpResourceScannerService,
509
@IEnvironmentService environmentService: IEnvironmentService
510
) {
511
super(mcpResource, ConfigurationTarget.USER, mcpGalleryService, fileService, uriIdentityService, logService, mcpResourceScannerService);
512
this.mcpLocation = uriIdentityService.extUri.joinPath(environmentService.userRoamingDataHome, 'mcp');
513
}
514
515
async installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {
516
throw new Error('Not supported');
517
}
518
519
async updateMetadata(local: ILocalMcpServer, gallery: IGalleryMcpServer): Promise<ILocalMcpServer> {
520
await this.updateMetadataFromGallery(gallery);
521
await this.updateLocal();
522
const updatedLocal = (await this.getInstalled()).find(s => s.name === local.name);
523
if (!updatedLocal) {
524
throw new Error(`Failed to find MCP server: ${local.name}`);
525
}
526
return updatedLocal;
527
}
528
529
protected async updateMetadataFromGallery(gallery: IGalleryMcpServer): Promise<IGalleryMcpServerConfiguration> {
530
const manifest = gallery.configuration;
531
const location = this.getLocation(gallery.name, gallery.version);
532
const manifestPath = this.uriIdentityService.extUri.joinPath(location, 'manifest.json');
533
const local: ILocalMcpServerInfo = {
534
galleryUrl: gallery.galleryUrl,
535
galleryId: gallery.id,
536
name: gallery.name,
537
displayName: gallery.displayName,
538
description: gallery.description,
539
version: gallery.version,
540
publisher: gallery.publisher,
541
publisherDisplayName: gallery.publisherDisplayName,
542
repositoryUrl: gallery.repositoryUrl,
543
licenseUrl: gallery.license,
544
icon: gallery.icon,
545
codicon: gallery.codicon,
546
manifest,
547
};
548
await this.fileService.writeFile(manifestPath, VSBuffer.fromString(JSON.stringify(local)));
549
550
if (gallery.readmeUrl || gallery.readme) {
551
const readme = gallery.readme ? gallery.readme : await this.mcpGalleryService.getReadme(gallery, CancellationToken.None);
552
await this.fileService.writeFile(this.uriIdentityService.extUri.joinPath(location, 'README.md'), VSBuffer.fromString(readme));
553
}
554
555
return manifest;
556
}
557
558
protected async getLocalServerInfo(name: string, mcpServerConfig: IMcpServerConfiguration): Promise<ILocalMcpServerInfo | undefined> {
559
let storedMcpServerInfo: ILocalMcpServerInfo | undefined;
560
let location: URI | undefined;
561
let readmeUrl: URI | undefined;
562
if (mcpServerConfig.gallery) {
563
location = this.getLocation(name, mcpServerConfig.version);
564
const manifestLocation = this.uriIdentityService.extUri.joinPath(location, 'manifest.json');
565
try {
566
const content = await this.fileService.readFile(manifestLocation);
567
storedMcpServerInfo = JSON.parse(content.value.toString()) as ILocalMcpServerInfo;
568
569
// migrate
570
if (storedMcpServerInfo.galleryUrl?.includes('/v0/')) {
571
storedMcpServerInfo.galleryUrl = storedMcpServerInfo.galleryUrl.substring(0, storedMcpServerInfo.galleryUrl.indexOf('/v0/'));
572
await this.fileService.writeFile(manifestLocation, VSBuffer.fromString(JSON.stringify(storedMcpServerInfo)));
573
}
574
575
storedMcpServerInfo.location = location;
576
readmeUrl = this.uriIdentityService.extUri.joinPath(location, 'README.md');
577
if (!await this.fileService.exists(readmeUrl)) {
578
readmeUrl = undefined;
579
}
580
storedMcpServerInfo.readmeUrl = readmeUrl;
581
} catch (e) {
582
this.logService.error('MCP Management Service: failed to read manifest', location.toString(), e);
583
}
584
}
585
return storedMcpServerInfo;
586
}
587
588
protected getLocation(name: string, version?: string): URI {
589
name = name.replace('/', '.');
590
return this.uriIdentityService.extUri.joinPath(this.mcpLocation, version ? `${name}-${version}` : name);
591
}
592
593
protected override installFromUri(uri: URI, options?: Omit<InstallOptions, 'mcpResource'>): Promise<ILocalMcpServer> {
594
throw new Error('Method not supported.');
595
}
596
597
override canInstall(): true | IMarkdownString {
598
throw new Error('Not supported');
599
}
600
601
}
602
603
export abstract class AbstractMcpManagementService extends AbstractCommonMcpManagementService implements IMcpManagementService {
604
605
constructor(
606
@IAllowedMcpServersService protected readonly allowedMcpServersService: IAllowedMcpServersService,
607
@ILogService logService: ILogService,
608
) {
609
super(logService);
610
}
611
612
canInstall(server: IGalleryMcpServer | IInstallableMcpServer): true | IMarkdownString {
613
const allowedToInstall = this.allowedMcpServersService.isAllowed(server);
614
if (allowedToInstall !== true) {
615
return new MarkdownString(localize('not allowed to install', "This mcp server cannot be installed because {0}", allowedToInstall.value));
616
}
617
return true;
618
}
619
}
620
621
export class McpManagementService extends AbstractMcpManagementService implements IMcpManagementService {
622
623
private readonly _onInstallMcpServer = this._register(new Emitter<InstallMcpServerEvent>());
624
readonly onInstallMcpServer = this._onInstallMcpServer.event;
625
626
private readonly _onDidInstallMcpServers = this._register(new Emitter<readonly InstallMcpServerResult[]>());
627
readonly onDidInstallMcpServers = this._onDidInstallMcpServers.event;
628
629
private readonly _onDidUpdateMcpServers = this._register(new Emitter<readonly InstallMcpServerResult[]>());
630
readonly onDidUpdateMcpServers = this._onDidUpdateMcpServers.event;
631
632
private readonly _onUninstallMcpServer = this._register(new Emitter<UninstallMcpServerEvent>());
633
readonly onUninstallMcpServer = this._onUninstallMcpServer.event;
634
635
private readonly _onDidUninstallMcpServer = this._register(new Emitter<DidUninstallMcpServerEvent>());
636
readonly onDidUninstallMcpServer = this._onDidUninstallMcpServer.event;
637
638
private readonly mcpResourceManagementServices = new ResourceMap<{ service: McpUserResourceManagementService } & IDisposable>();
639
640
constructor(
641
@IAllowedMcpServersService allowedMcpServersService: IAllowedMcpServersService,
642
@ILogService logService: ILogService,
643
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
644
@IInstantiationService protected readonly instantiationService: IInstantiationService,
645
) {
646
super(allowedMcpServersService, logService);
647
}
648
649
private getMcpResourceManagementService(mcpResource: URI): McpUserResourceManagementService {
650
let mcpResourceManagementService = this.mcpResourceManagementServices.get(mcpResource);
651
if (!mcpResourceManagementService) {
652
const disposables = new DisposableStore();
653
const service = disposables.add(this.createMcpResourceManagementService(mcpResource));
654
disposables.add(service.onInstallMcpServer(e => this._onInstallMcpServer.fire(e)));
655
disposables.add(service.onDidInstallMcpServers(e => this._onDidInstallMcpServers.fire(e)));
656
disposables.add(service.onDidUpdateMcpServers(e => this._onDidUpdateMcpServers.fire(e)));
657
disposables.add(service.onUninstallMcpServer(e => this._onUninstallMcpServer.fire(e)));
658
disposables.add(service.onDidUninstallMcpServer(e => this._onDidUninstallMcpServer.fire(e)));
659
this.mcpResourceManagementServices.set(mcpResource, mcpResourceManagementService = { service, dispose: () => disposables.dispose() });
660
}
661
return mcpResourceManagementService.service;
662
}
663
664
async getInstalled(mcpResource?: URI): Promise<ILocalMcpServer[]> {
665
const mcpResourceUri = mcpResource || this.userDataProfilesService.defaultProfile.mcpResource;
666
return this.getMcpResourceManagementService(mcpResourceUri).getInstalled();
667
}
668
669
async install(server: IInstallableMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {
670
const mcpResourceUri = options?.mcpResource || this.userDataProfilesService.defaultProfile.mcpResource;
671
return this.getMcpResourceManagementService(mcpResourceUri).install(server, options);
672
}
673
674
async uninstall(server: ILocalMcpServer, options?: UninstallOptions): Promise<void> {
675
const mcpResourceUri = options?.mcpResource || this.userDataProfilesService.defaultProfile.mcpResource;
676
return this.getMcpResourceManagementService(mcpResourceUri).uninstall(server, options);
677
}
678
679
async installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {
680
const mcpResourceUri = options?.mcpResource || this.userDataProfilesService.defaultProfile.mcpResource;
681
return this.getMcpResourceManagementService(mcpResourceUri).installFromGallery(server, options);
682
}
683
684
async updateMetadata(local: ILocalMcpServer, gallery: IGalleryMcpServer, mcpResource?: URI): Promise<ILocalMcpServer> {
685
return this.getMcpResourceManagementService(mcpResource || this.userDataProfilesService.defaultProfile.mcpResource).updateMetadata(local, gallery);
686
}
687
688
override dispose(): void {
689
this.mcpResourceManagementServices.forEach(service => service.dispose());
690
this.mcpResourceManagementServices.clear();
691
super.dispose();
692
}
693
694
protected createMcpResourceManagementService(mcpResource: URI): McpUserResourceManagementService {
695
return this.instantiationService.createInstance(McpUserResourceManagementService, mcpResource);
696
}
697
698
}
699
700