Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts
3296 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 { CancellationToken } from '../../../../base/common/cancellation.js';
7
import { Emitter, Event } from '../../../../base/common/event.js';
8
import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';
9
import { Disposable } from '../../../../base/common/lifecycle.js';
10
import { Schemas } from '../../../../base/common/network.js';
11
import { basename } from '../../../../base/common/resources.js';
12
import { Mutable } from '../../../../base/common/types.js';
13
import { URI } from '../../../../base/common/uri.js';
14
import { localize } from '../../../../nls.js';
15
import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
16
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
17
import { IEditorOptions } from '../../../../platform/editor/common/editor.js';
18
import { IFileService } from '../../../../platform/files/common/files.js';
19
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
20
import { ILabelService } from '../../../../platform/label/common/label.js';
21
import { ILogService } from '../../../../platform/log/common/log.js';
22
import { IGalleryMcpServer, IMcpGalleryService, IQueryOptions, IInstallableMcpServer, IGalleryMcpServerConfiguration, mcpAccessConfig, McpAccessValue } from '../../../../platform/mcp/common/mcpManagement.js';
23
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
24
import { IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration, McpServerType } from '../../../../platform/mcp/common/mcpPlatformTypes.js';
25
import { IProductService } from '../../../../platform/product/common/productService.js';
26
import { StorageScope } from '../../../../platform/storage/common/storage.js';
27
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
28
import { IURLService } from '../../../../platform/url/common/url.js';
29
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
30
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
31
import { IWorkbenchContribution } from '../../../common/contributions.js';
32
import { MCP_CONFIGURATION_KEY, WORKSPACE_STANDALONE_CONFIGURATIONS } from '../../../services/configuration/common/configuration.js';
33
import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js';
34
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
35
import { DidUninstallWorkbenchMcpServerEvent, IWorkbenchLocalMcpServer, IWorkbenchMcpManagementService, IWorkbenchMcpServerInstallResult, LocalMcpServerScope, REMOTE_USER_CONFIG_ID, USER_CONFIG_ID, WORKSPACE_CONFIG_ID, WORKSPACE_FOLDER_CONFIG_ID_PREFIX } from '../../../services/mcp/common/mcpWorkbenchManagementService.js';
36
import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';
37
import { mcpConfigurationSection } from '../common/mcpConfiguration.js';
38
import { McpServerInstallData, McpServerInstallClassification } from '../common/mcpServer.js';
39
import { HasInstalledMcpServersContext, IMcpConfigPath, IMcpWorkbenchService, IWorkbenchMcpServer, McpCollectionSortOrder, McpServerEnablementState, McpServerInstallState, McpServersGalleryStatusContext } from '../common/mcpTypes.js';
40
import { McpServerEditorInput } from './mcpServerEditorInput.js';
41
import { IMcpGalleryManifestService } from '../../../../platform/mcp/common/mcpGalleryManifest.js';
42
import { IPager, singlePagePager } from '../../../../base/common/paging.js';
43
44
interface IMcpServerStateProvider<T> {
45
(mcpWorkbenchServer: McpWorkbenchServer): T;
46
}
47
48
class McpWorkbenchServer implements IWorkbenchMcpServer {
49
50
constructor(
51
private installStateProvider: IMcpServerStateProvider<McpServerInstallState>,
52
public local: IWorkbenchLocalMcpServer | undefined,
53
public gallery: IGalleryMcpServer | undefined,
54
public readonly installable: IInstallableMcpServer | undefined,
55
@IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService,
56
@IFileService private readonly fileService: IFileService,
57
@IConfigurationService private readonly configurationService: IConfigurationService,
58
) {
59
this.local = local;
60
}
61
62
get id(): string {
63
return this.local?.id ?? this.gallery?.id ?? this.installable?.name ?? this.name;
64
}
65
66
get name(): string {
67
return this.gallery?.name ?? this.local?.name ?? this.installable?.name ?? '';
68
}
69
70
get label(): string {
71
return this.gallery?.displayName ?? this.local?.displayName ?? this.local?.name ?? this.installable?.name ?? '';
72
}
73
74
get icon(): {
75
readonly dark: string;
76
readonly light: string;
77
} | undefined {
78
return this.gallery?.icon ?? this.local?.icon;
79
}
80
81
get installState(): McpServerInstallState {
82
return this.installStateProvider(this);
83
}
84
85
get codicon(): string | undefined {
86
return this.gallery?.codicon ?? this.local?.codicon;
87
}
88
89
get publisherDisplayName(): string | undefined {
90
return this.gallery?.publisherDisplayName ?? this.local?.publisherDisplayName ?? this.gallery?.publisher ?? this.local?.publisher;
91
}
92
93
get publisherUrl(): string | undefined {
94
return this.gallery?.publisherDomain?.link;
95
}
96
97
get description(): string {
98
return this.gallery?.description ?? this.local?.description ?? '';
99
}
100
101
get starsCount(): number {
102
return this.gallery?.starsCount ?? 0;
103
}
104
105
get url(): string | undefined {
106
return this.gallery?.url;
107
}
108
109
get repository(): string | undefined {
110
return this.gallery?.repositoryUrl;
111
}
112
113
get config(): IMcpServerConfiguration | undefined {
114
return this.local?.config ?? this.installable?.config;
115
}
116
117
get enablementState(): McpServerEnablementState {
118
const accessValue = this.configurationService.getValue(mcpAccessConfig);
119
if (accessValue === McpAccessValue.None) {
120
return McpServerEnablementState.DisabledByAccess;
121
}
122
if (accessValue === McpAccessValue.Registry && !this.gallery) {
123
return McpServerEnablementState.DisabledByAccess;
124
}
125
return McpServerEnablementState.Enabled;
126
}
127
128
get readmeUrl(): URI | undefined {
129
return this.local?.readmeUrl ?? (this.gallery?.readmeUrl ? URI.parse(this.gallery.readmeUrl) : undefined);
130
}
131
132
async getReadme(token: CancellationToken): Promise<string> {
133
if (this.local?.readmeUrl) {
134
const content = await this.fileService.readFile(this.local.readmeUrl);
135
return content.value.toString();
136
}
137
138
if (this.gallery?.readme) {
139
return this.gallery.readme;
140
}
141
142
if (this.gallery?.readmeUrl) {
143
return this.mcpGalleryService.getReadme(this.gallery, token);
144
}
145
146
return Promise.reject(new Error('not available'));
147
}
148
149
async getManifest(token: CancellationToken): Promise<IGalleryMcpServerConfiguration> {
150
if (this.local?.manifest) {
151
return this.local.manifest;
152
}
153
154
if (this.gallery) {
155
return this.mcpGalleryService.getMcpServerConfiguration(this.gallery, token);
156
}
157
158
throw new Error('No manifest available');
159
}
160
161
}
162
163
export class McpWorkbenchService extends Disposable implements IMcpWorkbenchService {
164
165
_serviceBrand: undefined;
166
167
private installing: McpWorkbenchServer[] = [];
168
private uninstalling: McpWorkbenchServer[] = [];
169
170
private _local: McpWorkbenchServer[] = [];
171
get local(): readonly McpWorkbenchServer[] { return [...this._local]; }
172
173
private readonly _onChange = this._register(new Emitter<IWorkbenchMcpServer | undefined>());
174
readonly onChange = this._onChange.event;
175
176
private readonly _onReset = this._register(new Emitter<void>());
177
readonly onReset = this._onReset.event;
178
179
constructor(
180
@IMcpGalleryManifestService mcpGalleryManifestService: IMcpGalleryManifestService,
181
@IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService,
182
@IWorkbenchMcpManagementService private readonly mcpManagementService: IWorkbenchMcpManagementService,
183
@IEditorService private readonly editorService: IEditorService,
184
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
185
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
186
@IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService,
187
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
188
@ILabelService private readonly labelService: ILabelService,
189
@IProductService private readonly productService: IProductService,
190
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
191
@IConfigurationService private readonly configurationService: IConfigurationService,
192
@IInstantiationService private readonly instantiationService: IInstantiationService,
193
@ITelemetryService private readonly telemetryService: ITelemetryService,
194
@ILogService private readonly logService: ILogService,
195
@IURLService urlService: IURLService,
196
) {
197
super();
198
this._register(this.mcpManagementService.onDidInstallMcpServersInCurrentProfile(e => this.onDidInstallMcpServers(e)));
199
this._register(this.mcpManagementService.onDidUpdateMcpServersInCurrentProfile(e => this.onDidUpdateMcpServers(e)));
200
this._register(this.mcpManagementService.onDidUninstallMcpServerInCurrentProfile(e => this.onDidUninstallMcpServer(e)));
201
this._register(this.mcpManagementService.onDidChangeProfile(e => this.onDidChangeProfile()));
202
this.queryLocal().then(() => this.syncInstalledMcpServers());
203
urlService.registerHandler(this);
204
this._register(this.configurationService.onDidChangeConfiguration(e => {
205
if (e.affectsConfiguration(mcpAccessConfig)) {
206
this._onChange.fire(undefined);
207
}
208
}));
209
this._register(mcpGalleryManifestService.onDidChangeMcpGalleryManifest(e => this.syncInstalledMcpServers(true)));
210
}
211
212
private async onDidChangeProfile() {
213
await this.queryLocal();
214
this._onChange.fire(undefined);
215
this._onReset.fire();
216
}
217
218
private areSameMcpServers(a: { name: string; scope: LocalMcpServerScope } | undefined, b: { name: string; scope: LocalMcpServerScope } | undefined): boolean {
219
if (a === b) {
220
return true;
221
}
222
if (!a || !b) {
223
return false;
224
}
225
return a.name === b.name && a.scope === b.scope;
226
}
227
228
private onDidUninstallMcpServer(e: DidUninstallWorkbenchMcpServerEvent) {
229
if (e.error) {
230
return;
231
}
232
const uninstalled = this._local.find(server => this.areSameMcpServers(server.local, e));
233
if (uninstalled) {
234
this._local = this._local.filter(server => server !== uninstalled);
235
this._onChange.fire(uninstalled);
236
}
237
}
238
239
private onDidInstallMcpServers(e: readonly IWorkbenchMcpServerInstallResult[]) {
240
const servers: IWorkbenchMcpServer[] = [];
241
for (const { local, source, name } of e) {
242
let server = this.installing.find(server => server.local && local ? this.areSameMcpServers(server.local, local) : server.name === name);
243
this.installing = server ? this.installing.filter(e => e !== server) : this.installing;
244
if (local) {
245
if (server) {
246
server.local = local;
247
} else {
248
server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), local, source, undefined);
249
}
250
if (!local.galleryUrl) {
251
server.gallery = undefined;
252
}
253
this._local = this._local.filter(server => !this.areSameMcpServers(server.local, local));
254
this._local.push(server);
255
}
256
this._onChange.fire(server);
257
}
258
if (servers.some(server => server.local?.galleryUrl && !server.gallery)) {
259
this.syncInstalledMcpServers();
260
}
261
}
262
263
private onDidUpdateMcpServers(e: readonly IWorkbenchMcpServerInstallResult[]) {
264
for (const result of e) {
265
if (!result.local) {
266
continue;
267
}
268
const serverIndex = this._local.findIndex(server => this.areSameMcpServers(server.local, result.local));
269
let server: McpWorkbenchServer;
270
if (serverIndex !== -1) {
271
this._local[serverIndex].local = result.local;
272
server = this._local[serverIndex];
273
} else {
274
server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), result.local, result.source, undefined);
275
this._local.push(server);
276
}
277
this._onChange.fire(server);
278
}
279
}
280
281
private fromGallery(gallery: IGalleryMcpServer): IWorkbenchMcpServer | undefined {
282
for (const local of this._local) {
283
if (local.name === gallery.name) {
284
local.gallery = gallery;
285
return local;
286
}
287
}
288
return undefined;
289
}
290
291
private async syncInstalledMcpServers(resetGallery?: boolean): Promise<void> {
292
const galleryMcpServerUrls: string[] = [];
293
const vscodeGalleryMcpServerNames: string[] = [];
294
295
for (const installed of this.local) {
296
if (installed.local?.source !== 'gallery') {
297
continue;
298
}
299
if (installed.local.galleryUrl) {
300
galleryMcpServerUrls.push(installed.local.galleryUrl);
301
} else if (!installed.local.manifest) {
302
vscodeGalleryMcpServerNames.push(installed.local.name);
303
}
304
}
305
306
if (galleryMcpServerUrls.length) {
307
const galleryServers = await this.mcpGalleryService.getMcpServersFromGallery(galleryMcpServerUrls);
308
if (galleryServers.length) {
309
await this.syncInstalledMcpServersWithGallery(galleryServers, false, resetGallery);
310
}
311
}
312
313
if (vscodeGalleryMcpServerNames.length) {
314
const galleryServers = await this.mcpGalleryService.getMcpServersFromVSCodeGallery(vscodeGalleryMcpServerNames);
315
if (galleryServers.length) {
316
await this.syncInstalledMcpServersWithGallery(galleryServers, true, resetGallery);
317
}
318
}
319
}
320
321
private async syncInstalledMcpServersWithGallery(gallery: IGalleryMcpServer[], vscodeGallery: boolean, resetGallery?: boolean): Promise<void> {
322
const galleryMap = new Map<string, IGalleryMcpServer>(gallery.map(server => [vscodeGallery ? server.name : (server.url ?? server.name), server]));
323
for (const mcpServer of this.local) {
324
if (!mcpServer.local) {
325
continue;
326
}
327
const key = vscodeGallery ? mcpServer.local.name : mcpServer.local.galleryUrl;
328
const galleryServer = key ? galleryMap.get(key) : undefined;
329
if (!galleryServer) {
330
if (mcpServer.gallery && resetGallery) {
331
mcpServer.gallery = undefined;
332
this._onChange.fire(mcpServer);
333
}
334
continue;
335
}
336
if (!vscodeGallery) {
337
mcpServer.gallery = galleryServer;
338
}
339
if (!mcpServer.local.manifest) {
340
mcpServer.local = await this.mcpManagementService.updateMetadata(mcpServer.local, galleryServer);
341
}
342
this._onChange.fire(mcpServer);
343
}
344
}
345
346
async queryGallery(options?: IQueryOptions, token?: CancellationToken): Promise<IPager<IWorkbenchMcpServer>> {
347
if (!this.mcpGalleryService.isEnabled()) {
348
return singlePagePager([]);
349
}
350
const pager = await this.mcpGalleryService.query(options, token);
351
return {
352
firstPage: pager.firstPage.map(gallery => this.fromGallery(gallery) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, gallery, undefined)),
353
total: pager.total,
354
pageSize: pager.pageSize,
355
getPage: async (pageIndex, token) => {
356
const page = await pager.getPage(pageIndex, token);
357
return page.map(gallery => this.fromGallery(gallery) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, gallery, undefined));
358
}
359
};
360
}
361
362
async queryLocal(): Promise<IWorkbenchMcpServer[]> {
363
const installed = await this.mcpManagementService.getInstalled();
364
this._local = installed.map(i => {
365
const existing = this._local.find(local => {
366
if (i.galleryUrl) {
367
return local.local?.galleryUrl === i.galleryUrl;
368
}
369
return local.id === i.id;
370
});
371
const local = existing ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, undefined, undefined);
372
local.local = i;
373
return local;
374
});
375
this._onChange.fire(undefined);
376
return [...this.local];
377
}
378
379
getEnabledLocalMcpServers(): IWorkbenchLocalMcpServer[] {
380
const result = new Map<string, IWorkbenchLocalMcpServer>();
381
const userRemote: IWorkbenchLocalMcpServer[] = [];
382
const workspace: IWorkbenchLocalMcpServer[] = [];
383
384
for (const server of this.local) {
385
if (server.enablementState !== McpServerEnablementState.Enabled) {
386
continue;
387
}
388
389
if (server.local?.scope === LocalMcpServerScope.User) {
390
result.set(server.name, server.local);
391
} else if (server.local?.scope === LocalMcpServerScope.RemoteUser) {
392
userRemote.push(server.local);
393
} else if (server.local?.scope === LocalMcpServerScope.Workspace) {
394
workspace.push(server.local);
395
}
396
}
397
398
for (const server of userRemote) {
399
const existing = result.get(server.name);
400
if (existing) {
401
this.logService.warn(localize('overwriting', "Overwriting mcp server '{0}' from {1} with {2}.", server.name, server.mcpResource.path, existing.mcpResource.path));
402
}
403
result.set(server.name, server);
404
}
405
406
for (const server of workspace) {
407
const existing = result.get(server.name);
408
if (existing) {
409
this.logService.warn(localize('overwriting', "Overwriting mcp server '{0}' from {1} with {2}.", server.name, server.mcpResource.path, existing.mcpResource.path));
410
}
411
result.set(server.name, server);
412
}
413
414
return [...result.values()];
415
}
416
417
canInstall(mcpServer: IWorkbenchMcpServer): true | IMarkdownString {
418
if (!(mcpServer instanceof McpWorkbenchServer)) {
419
return new MarkdownString().appendText(localize('not an extension', "The provided object is not an mcp server."));
420
}
421
422
if (mcpServer.gallery) {
423
const result = this.mcpManagementService.canInstall(mcpServer.gallery);
424
if (result === true) {
425
return true;
426
}
427
428
return result;
429
}
430
431
if (mcpServer.installable) {
432
const result = this.mcpManagementService.canInstall(mcpServer.installable);
433
if (result === true) {
434
return true;
435
}
436
437
return result;
438
}
439
440
441
return new MarkdownString().appendText(localize('cannot be installed', "Cannot install the '{0}' MCP Server because it is not available in this setup.", mcpServer.label));
442
}
443
444
async install(server: IWorkbenchMcpServer): Promise<IWorkbenchMcpServer> {
445
if (!(server instanceof McpWorkbenchServer)) {
446
throw new Error('Invalid server instance');
447
}
448
449
if (server.installable) {
450
const installable = server.installable;
451
return this.doInstall(server, () => this.mcpManagementService.install(installable));
452
}
453
454
if (server.gallery) {
455
const gallery = server.gallery;
456
return this.doInstall(server, () => this.mcpManagementService.installFromGallery(gallery));
457
}
458
459
throw new Error('No installable server found');
460
}
461
462
async uninstall(server: IWorkbenchMcpServer): Promise<void> {
463
if (!server.local) {
464
throw new Error('Local server is missing');
465
}
466
await this.mcpManagementService.uninstall(server.local);
467
}
468
469
private async doInstall(server: McpWorkbenchServer, installTask: () => Promise<IWorkbenchLocalMcpServer>): Promise<IWorkbenchMcpServer> {
470
const source = server.gallery ? 'gallery' : 'local';
471
const serverName = server.name;
472
// Check for inputs in installable config or if it comes from handleURL with inputs
473
const hasInputs = !!(server.installable?.inputs && server.installable.inputs.length > 0);
474
475
this.installing.push(server);
476
this._onChange.fire(server);
477
478
try {
479
await installTask();
480
const result = await this.waitAndGetInstalledMcpServer(server);
481
482
// Track successful installation
483
this.telemetryService.publicLog2<McpServerInstallData, McpServerInstallClassification>('mcp/serverInstall', {
484
serverName,
485
source,
486
scope: result.local?.scope ?? 'unknown',
487
success: true,
488
hasInputs
489
});
490
491
return result;
492
} catch (error) {
493
// Track failed installation
494
this.telemetryService.publicLog2<McpServerInstallData, McpServerInstallClassification>('mcp/serverInstall', {
495
serverName,
496
source,
497
scope: 'unknown',
498
success: false,
499
error: error instanceof Error ? error.message : String(error),
500
hasInputs
501
});
502
503
throw error;
504
}
505
}
506
507
private async waitAndGetInstalledMcpServer(server: McpWorkbenchServer): Promise<IWorkbenchMcpServer> {
508
let installed = this.local.find(local => local.name === server.name);
509
if (!installed) {
510
await Event.toPromise(Event.filter(this.onChange, e => !!e && this.local.some(local => local.name === server.name)));
511
}
512
installed = this.local.find(local => local.name === server.name);
513
if (!installed) {
514
// This should not happen
515
throw new Error('Extension should have been installed');
516
}
517
return installed;
518
}
519
520
getMcpConfigPath(localMcpServer: IWorkbenchLocalMcpServer): IMcpConfigPath | undefined;
521
getMcpConfigPath(mcpResource: URI): Promise<IMcpConfigPath | undefined>;
522
getMcpConfigPath(arg: URI | IWorkbenchLocalMcpServer): Promise<IMcpConfigPath | undefined> | IMcpConfigPath | undefined {
523
if (arg instanceof URI) {
524
const mcpResource = arg;
525
for (const profile of this.userDataProfilesService.profiles) {
526
if (this.uriIdentityService.extUri.isEqual(profile.mcpResource, mcpResource)) {
527
return this.getUserMcpConfigPath(mcpResource);
528
}
529
}
530
531
return this.remoteAgentService.getEnvironment().then(remoteEnvironment => {
532
if (remoteEnvironment && this.uriIdentityService.extUri.isEqual(remoteEnvironment.mcpResource, mcpResource)) {
533
return this.getRemoteMcpConfigPath(mcpResource);
534
}
535
return this.getWorkspaceMcpConfigPath(mcpResource);
536
});
537
}
538
539
if (arg.scope === LocalMcpServerScope.User) {
540
return this.getUserMcpConfigPath(arg.mcpResource);
541
}
542
543
if (arg.scope === LocalMcpServerScope.Workspace) {
544
return this.getWorkspaceMcpConfigPath(arg.mcpResource);
545
}
546
547
if (arg.scope === LocalMcpServerScope.RemoteUser) {
548
return this.getRemoteMcpConfigPath(arg.mcpResource);
549
}
550
551
return undefined;
552
}
553
554
private getUserMcpConfigPath(mcpResource: URI): IMcpConfigPath {
555
return {
556
id: USER_CONFIG_ID,
557
key: 'userLocalValue',
558
target: ConfigurationTarget.USER_LOCAL,
559
label: localize('mcp.configuration.userLocalValue', 'Global in {0}', this.productService.nameShort),
560
scope: StorageScope.PROFILE,
561
order: McpCollectionSortOrder.User,
562
uri: mcpResource,
563
section: [],
564
};
565
}
566
567
private getRemoteMcpConfigPath(mcpResource: URI): IMcpConfigPath {
568
return {
569
id: REMOTE_USER_CONFIG_ID,
570
key: 'userRemoteValue',
571
target: ConfigurationTarget.USER_REMOTE,
572
label: this.environmentService.remoteAuthority ? this.labelService.getHostLabel(Schemas.vscodeRemote, this.environmentService.remoteAuthority) : 'Remote',
573
scope: StorageScope.PROFILE,
574
order: McpCollectionSortOrder.User + McpCollectionSortOrder.RemoteBoost,
575
remoteAuthority: this.environmentService.remoteAuthority,
576
uri: mcpResource,
577
section: [],
578
};
579
}
580
581
private getWorkspaceMcpConfigPath(mcpResource: URI): IMcpConfigPath | undefined {
582
const workspace = this.workspaceService.getWorkspace();
583
if (workspace.configuration && this.uriIdentityService.extUri.isEqual(workspace.configuration, mcpResource)) {
584
return {
585
id: WORKSPACE_CONFIG_ID,
586
key: 'workspaceValue',
587
target: ConfigurationTarget.WORKSPACE,
588
label: basename(mcpResource),
589
scope: StorageScope.WORKSPACE,
590
order: McpCollectionSortOrder.Workspace,
591
remoteAuthority: this.environmentService.remoteAuthority,
592
uri: mcpResource,
593
section: ['settings', mcpConfigurationSection],
594
};
595
}
596
597
const workspaceFolders = workspace.folders;
598
for (let index = 0; index < workspaceFolders.length; index++) {
599
const workspaceFolder = workspaceFolders[index];
600
if (this.uriIdentityService.extUri.isEqual(this.uriIdentityService.extUri.joinPath(workspaceFolder.uri, WORKSPACE_STANDALONE_CONFIGURATIONS[MCP_CONFIGURATION_KEY]), mcpResource)) {
601
return {
602
id: `${WORKSPACE_FOLDER_CONFIG_ID_PREFIX}${index}`,
603
key: 'workspaceFolderValue',
604
target: ConfigurationTarget.WORKSPACE_FOLDER,
605
label: `${workspaceFolder.name}/.vscode/mcp.json`,
606
scope: StorageScope.WORKSPACE,
607
remoteAuthority: this.environmentService.remoteAuthority,
608
order: McpCollectionSortOrder.WorkspaceFolder,
609
uri: mcpResource,
610
workspaceFolder,
611
};
612
}
613
}
614
615
return undefined;
616
}
617
618
async handleURL(uri: URI): Promise<boolean> {
619
if (uri.path === 'mcp/install') {
620
return this.handleMcpInstallUri(uri);
621
}
622
if (uri.path.startsWith('mcp/')) {
623
const mcpServerUrl = uri.path.substring(4);
624
if (mcpServerUrl) {
625
return this.handleMcpServerUrl(`${Schemas.https}://${mcpServerUrl}`);
626
}
627
}
628
return false;
629
}
630
631
private async handleMcpInstallUri(uri: URI): Promise<boolean> {
632
let parsed: IMcpServerConfiguration & { name: string; inputs?: IMcpServerVariable[]; gallery?: boolean };
633
try {
634
parsed = JSON.parse(decodeURIComponent(uri.query));
635
} catch (e) {
636
return false;
637
}
638
639
try {
640
const { name, inputs, gallery, ...config } = parsed;
641
642
if (gallery || !config || Object.keys(config).length === 0) {
643
const [galleryServer] = await this.mcpGalleryService.getMcpServersFromVSCodeGallery([name]);
644
if (!galleryServer) {
645
throw new Error(`MCP server '${name}' not found in gallery`);
646
}
647
const local = this.local.find(e => e.name === name && e.local?.scope !== LocalMcpServerScope.Workspace)
648
?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, galleryServer, undefined);
649
this.open(local);
650
} else {
651
if (config.type === undefined) {
652
(<Mutable<IMcpServerConfiguration>>config).type = (<IMcpStdioServerConfiguration>parsed).command ? McpServerType.LOCAL : McpServerType.REMOTE;
653
}
654
this.open(this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, undefined, { name, config, inputs }));
655
}
656
} catch (e) {
657
// ignore
658
}
659
return true;
660
}
661
662
private async handleMcpServerUrl(url: string): Promise<boolean> {
663
try {
664
const gallery = await this.mcpGalleryService.getMcpServer(url);
665
if (!gallery) {
666
this.logService.info(`MCP server '${url}' not found`);
667
return true;
668
}
669
const local = this.local.find(e => e.url === url) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, gallery, undefined);
670
this.open(local);
671
} catch (e) {
672
// ignore
673
this.logService.error(e);
674
}
675
return true;
676
}
677
678
async open(extension: IWorkbenchMcpServer, options?: IEditorOptions): Promise<void> {
679
await this.editorService.openEditor(this.instantiationService.createInstance(McpServerEditorInput, extension), options, ACTIVE_GROUP);
680
}
681
682
private getInstallState(extension: McpWorkbenchServer): McpServerInstallState {
683
if (this.installing.some(i => i.name === extension.name)) {
684
return McpServerInstallState.Installing;
685
}
686
if (this.uninstalling.some(e => e.name === extension.name)) {
687
return McpServerInstallState.Uninstalling;
688
}
689
const local = this.local.find(e => e === extension);
690
return local ? McpServerInstallState.Installed : McpServerInstallState.Uninstalled;
691
}
692
693
}
694
695
export class MCPContextsInitialisation extends Disposable implements IWorkbenchContribution {
696
697
static ID = 'workbench.mcp.contexts.initialisation';
698
699
constructor(
700
@IMcpWorkbenchService mcpWorkbenchService: IMcpWorkbenchService,
701
@IMcpGalleryManifestService mcpGalleryManifestService: IMcpGalleryManifestService,
702
@IContextKeyService contextKeyService: IContextKeyService,
703
) {
704
super();
705
706
const mcpServersGalleryStatus = McpServersGalleryStatusContext.bindTo(contextKeyService);
707
mcpServersGalleryStatus.set(mcpGalleryManifestService.mcpGalleryManifestStatus);
708
this._register(mcpGalleryManifestService.onDidChangeMcpGalleryManifestStatus(status => mcpServersGalleryStatus.set(status)));
709
710
const hasInstalledMcpServersContextKey = HasInstalledMcpServersContext.bindTo(contextKeyService);
711
hasInstalledMcpServersContextKey.set(mcpWorkbenchService.local.length > 0);
712
this._register(mcpWorkbenchService.onChange(() => hasInstalledMcpServersContextKey.set(mcpWorkbenchService.local.length > 0)));
713
}
714
}
715
716