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
5257 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 { createCommandUri, 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, IAllowedMcpServersService } 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 { IEditorService, MODAL_GROUP } from '../../../services/editor/common/editorService.js';
34
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
35
import { DidUninstallWorkbenchMcpServerEvent, IWorkbenchLocalMcpServer, IWorkbenchMcpManagementService, IWorkbenchMcpServerInstallResult, IWorkbencMcpServerInstallOptions, 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, IMcpService, IMcpWorkbenchService, IWorkbenchMcpServer, McpCollectionSortOrder, McpServerEnablementState, McpServerInstallState, McpServerEnablementStatus, McpServersGalleryStatusContext } from '../common/mcpTypes.js';
40
import { McpServerEditorInput } from './mcpServerEditorInput.js';
41
import { IMcpGalleryManifestService } from '../../../../platform/mcp/common/mcpGalleryManifest.js';
42
import { IIterativePager, IIterativePage } from '../../../../base/common/paging.js';
43
import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
44
import { runOnChange } from '../../../../base/common/observable.js';
45
import Severity from '../../../../base/common/severity.js';
46
import { Queue } from '../../../../base/common/async.js';
47
48
interface IMcpServerStateProvider<T> {
49
(mcpWorkbenchServer: McpWorkbenchServer): T;
50
}
51
52
class McpWorkbenchServer implements IWorkbenchMcpServer {
53
54
constructor(
55
private installStateProvider: IMcpServerStateProvider<McpServerInstallState>,
56
private runtimeStateProvider: IMcpServerStateProvider<McpServerEnablementStatus | undefined>,
57
public local: IWorkbenchLocalMcpServer | undefined,
58
public gallery: IGalleryMcpServer | undefined,
59
public readonly installable: IInstallableMcpServer | undefined,
60
@IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService,
61
@IFileService private readonly fileService: IFileService,
62
) {
63
this.local = local;
64
}
65
66
get id(): string {
67
return this.local?.id ?? this.gallery?.name ?? this.installable?.name ?? this.name;
68
}
69
70
get name(): string {
71
return this.gallery?.name ?? this.local?.name ?? this.installable?.name ?? '';
72
}
73
74
get label(): string {
75
return this.gallery?.displayName ?? this.local?.displayName ?? this.local?.name ?? this.installable?.name ?? '';
76
}
77
78
get icon(): {
79
readonly dark: string;
80
readonly light: string;
81
} | undefined {
82
return this.gallery?.icon ?? this.local?.icon;
83
}
84
85
get installState(): McpServerInstallState {
86
return this.installStateProvider(this);
87
}
88
89
get codicon(): string | undefined {
90
return this.gallery?.codicon ?? this.local?.codicon;
91
}
92
93
get publisherDisplayName(): string | undefined {
94
return this.gallery?.publisherDisplayName ?? this.local?.publisherDisplayName ?? this.gallery?.publisher ?? this.local?.publisher;
95
}
96
97
get publisherUrl(): string | undefined {
98
return this.gallery?.publisherDomain?.link;
99
}
100
101
get description(): string {
102
return this.gallery?.description ?? this.local?.description ?? '';
103
}
104
105
get starsCount(): number {
106
return this.gallery?.starsCount ?? 0;
107
}
108
109
get license(): string | undefined {
110
return this.gallery?.license;
111
}
112
113
get repository(): string | undefined {
114
return this.gallery?.repositoryUrl;
115
}
116
117
get config(): IMcpServerConfiguration | undefined {
118
return this.local?.config ?? this.installable?.config;
119
}
120
121
get runtimeStatus(): McpServerEnablementStatus | undefined {
122
return this.runtimeStateProvider(this);
123
}
124
125
get readmeUrl(): URI | undefined {
126
return this.local?.readmeUrl ?? (this.gallery?.readmeUrl ? URI.parse(this.gallery.readmeUrl) : undefined);
127
}
128
129
async getReadme(token: CancellationToken): Promise<string> {
130
if (this.local?.readmeUrl) {
131
const content = await this.fileService.readFile(this.local.readmeUrl);
132
return content.value.toString();
133
}
134
135
if (this.gallery?.readme) {
136
return this.gallery.readme;
137
}
138
139
if (this.gallery?.readmeUrl) {
140
return this.mcpGalleryService.getReadme(this.gallery, token);
141
}
142
143
return Promise.reject(new Error('not available'));
144
}
145
146
async getManifest(token: CancellationToken): Promise<IGalleryMcpServerConfiguration> {
147
if (this.local?.manifest) {
148
return this.local.manifest;
149
}
150
151
if (this.gallery) {
152
return this.gallery.configuration;
153
}
154
155
throw new Error('No manifest available');
156
}
157
158
}
159
160
export class McpWorkbenchService extends Disposable implements IMcpWorkbenchService {
161
162
_serviceBrand: undefined;
163
164
private installing: McpWorkbenchServer[] = [];
165
private uninstalling: McpWorkbenchServer[] = [];
166
167
private _local: McpWorkbenchServer[] = [];
168
get local(): readonly McpWorkbenchServer[] { return [...this._local]; }
169
170
private readonly _onChange = this._register(new Emitter<IWorkbenchMcpServer | undefined>());
171
readonly onChange = this._onChange.event;
172
173
private readonly _onReset = this._register(new Emitter<void>());
174
readonly onReset = this._onReset.event;
175
176
constructor(
177
@IMcpGalleryManifestService mcpGalleryManifestService: IMcpGalleryManifestService,
178
@IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService,
179
@IWorkbenchMcpManagementService private readonly mcpManagementService: IWorkbenchMcpManagementService,
180
@IEditorService private readonly editorService: IEditorService,
181
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
182
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
183
@IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService,
184
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
185
@ILabelService private readonly labelService: ILabelService,
186
@IProductService private readonly productService: IProductService,
187
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
188
@IConfigurationService private readonly configurationService: IConfigurationService,
189
@IInstantiationService private readonly instantiationService: IInstantiationService,
190
@ITelemetryService private readonly telemetryService: ITelemetryService,
191
@ILogService private readonly logService: ILogService,
192
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
193
@IAllowedMcpServersService private readonly allowedMcpServersService: IAllowedMcpServersService,
194
@IMcpService private readonly mcpService: IMcpService,
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(() => {
203
if (this._store.isDisposed) {
204
return;
205
}
206
const queue = this._register(new Queue());
207
this._register(mcpGalleryManifestService.onDidChangeMcpGalleryManifest(e => queue.queue(() => this.syncInstalledMcpServers())));
208
queue.queue(() => this.syncInstalledMcpServers());
209
});
210
urlService.registerHandler(this);
211
this._register(this.configurationService.onDidChangeConfiguration(e => {
212
if (e.affectsConfiguration(mcpAccessConfig)) {
213
this._onChange.fire(undefined);
214
}
215
}));
216
this._register(this.allowedMcpServersService.onDidChangeAllowedMcpServers(() => {
217
this._local = this.sort(this._local);
218
this._onChange.fire(undefined);
219
}));
220
this._register(runOnChange(mcpService.servers, () => {
221
this._local = this.sort(this._local);
222
this._onChange.fire(undefined);
223
}));
224
}
225
226
private async onDidChangeProfile() {
227
await this.queryLocal();
228
this._onChange.fire(undefined);
229
this._onReset.fire();
230
}
231
232
private areSameMcpServers(a: { name: string; scope: LocalMcpServerScope } | undefined, b: { name: string; scope: LocalMcpServerScope } | undefined): boolean {
233
if (a === b) {
234
return true;
235
}
236
if (!a || !b) {
237
return false;
238
}
239
return a.name === b.name && a.scope === b.scope;
240
}
241
242
private onDidUninstallMcpServer(e: DidUninstallWorkbenchMcpServerEvent) {
243
if (e.error) {
244
return;
245
}
246
const uninstalled = this._local.find(server => this.areSameMcpServers(server.local, e));
247
if (uninstalled) {
248
this._local = this._local.filter(server => server !== uninstalled);
249
this._onChange.fire(uninstalled);
250
}
251
}
252
253
private onDidInstallMcpServers(e: readonly IWorkbenchMcpServerInstallResult[]) {
254
const servers: IWorkbenchMcpServer[] = [];
255
for (const { local, source, name } of e) {
256
let server = this.installing.find(server => server.local && local ? this.areSameMcpServers(server.local, local) : server.name === name);
257
this.installing = server ? this.installing.filter(e => e !== server) : this.installing;
258
if (local) {
259
if (server) {
260
server.local = local;
261
} else {
262
server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), local, source, undefined);
263
}
264
if (!local.galleryUrl) {
265
server.gallery = undefined;
266
}
267
this._local = this._local.filter(server => !this.areSameMcpServers(server.local, local));
268
this.addServer(server);
269
}
270
this._onChange.fire(server);
271
}
272
if (servers.some(server => server.local?.galleryUrl && !server.gallery)) {
273
this.syncInstalledMcpServers();
274
}
275
}
276
277
private onDidUpdateMcpServers(e: readonly IWorkbenchMcpServerInstallResult[]) {
278
for (const result of e) {
279
if (!result.local) {
280
continue;
281
}
282
const serverIndex = this._local.findIndex(server => this.areSameMcpServers(server.local, result.local));
283
let server: McpWorkbenchServer;
284
if (serverIndex !== -1) {
285
this._local[serverIndex].local = result.local;
286
server = this._local[serverIndex];
287
} else {
288
server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), result.local, result.source, undefined);
289
this.addServer(server);
290
}
291
this._onChange.fire(server);
292
}
293
}
294
295
private fromGallery(gallery: IGalleryMcpServer): IWorkbenchMcpServer | undefined {
296
for (const local of this._local) {
297
if (local.name === gallery.name) {
298
local.gallery = gallery;
299
return local;
300
}
301
}
302
return undefined;
303
}
304
305
private async syncInstalledMcpServers(): Promise<void> {
306
const infos: { name: string; id?: string }[] = [];
307
308
for (const installed of this.local) {
309
if (installed.local?.source !== 'gallery') {
310
continue;
311
}
312
if (installed.local.galleryUrl) {
313
infos.push({ name: installed.local.name, id: installed.local.galleryId });
314
}
315
}
316
317
if (infos.length) {
318
const galleryServers = await this.mcpGalleryService.getMcpServersFromGallery(infos);
319
await this.syncInstalledMcpServersWithGallery(galleryServers);
320
}
321
}
322
323
private async syncInstalledMcpServersWithGallery(gallery: IGalleryMcpServer[]): Promise<void> {
324
const galleryMap = new Map<string, IGalleryMcpServer>(gallery.map(server => [server.name, server]));
325
for (const mcpServer of this.local) {
326
if (!mcpServer.local) {
327
continue;
328
}
329
const key = mcpServer.local.name;
330
const gallery = key ? galleryMap.get(key) : undefined;
331
332
if (!gallery || gallery.galleryUrl !== mcpServer.local.galleryUrl) {
333
if (mcpServer.gallery) {
334
mcpServer.gallery = undefined;
335
this._onChange.fire(mcpServer);
336
}
337
continue;
338
}
339
340
mcpServer.gallery = gallery;
341
if (!mcpServer.local.manifest) {
342
mcpServer.local = await this.mcpManagementService.updateMetadata(mcpServer.local, gallery);
343
}
344
this._onChange.fire(mcpServer);
345
}
346
}
347
348
async queryGallery(options?: IQueryOptions, token?: CancellationToken): Promise<IIterativePager<IWorkbenchMcpServer>> {
349
if (!this.mcpGalleryService.isEnabled()) {
350
return {
351
firstPage: { items: [], hasMore: false },
352
getNextPage: async () => ({ items: [], hasMore: false })
353
};
354
}
355
const pager = await this.mcpGalleryService.query(options, token);
356
357
const mapPage = (page: IIterativePage<IGalleryMcpServer>): IIterativePage<IWorkbenchMcpServer> => ({
358
items: page.items.map(gallery => this.fromGallery(gallery) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, gallery, undefined)),
359
hasMore: page.hasMore
360
});
361
362
return {
363
firstPage: mapPage(pager.firstPage),
364
getNextPage: async (ct) => {
365
const nextPage = await pager.getNextPage(ct);
366
return mapPage(nextPage);
367
}
368
};
369
}
370
371
async queryLocal(): Promise<IWorkbenchMcpServer[]> {
372
const installed = await this.mcpManagementService.getInstalled();
373
this._local = this.sort(installed.map(i => {
374
const existing = this._local.find(local => local.id === i.id);
375
const local = existing ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, undefined, undefined);
376
local.local = i;
377
return local;
378
}));
379
this._onChange.fire(undefined);
380
return [...this.local];
381
}
382
383
private addServer(server: McpWorkbenchServer): void {
384
this._local.push(server);
385
this._local = this.sort(this._local);
386
}
387
388
private sort(local: McpWorkbenchServer[]): McpWorkbenchServer[] {
389
return local.sort((a, b) => {
390
if (a.name === b.name) {
391
if (!a.runtimeStatus || a.runtimeStatus.state === McpServerEnablementState.Enabled) {
392
return -1;
393
}
394
if (!b.runtimeStatus || b.runtimeStatus.state === McpServerEnablementState.Enabled) {
395
return 1;
396
}
397
return 0;
398
}
399
return a.name.localeCompare(b.name);
400
});
401
}
402
403
getEnabledLocalMcpServers(): IWorkbenchLocalMcpServer[] {
404
const result = new Map<string, IWorkbenchLocalMcpServer>();
405
const userRemote: IWorkbenchLocalMcpServer[] = [];
406
const workspace: IWorkbenchLocalMcpServer[] = [];
407
408
for (const server of this.local) {
409
const enablementStatus = this.getEnablementStatus(server);
410
if (enablementStatus && enablementStatus.state !== McpServerEnablementState.Enabled) {
411
continue;
412
}
413
414
if (server.local?.scope === LocalMcpServerScope.User) {
415
result.set(server.name, server.local);
416
} else if (server.local?.scope === LocalMcpServerScope.RemoteUser) {
417
userRemote.push(server.local);
418
} else if (server.local?.scope === LocalMcpServerScope.Workspace) {
419
workspace.push(server.local);
420
}
421
}
422
423
for (const server of userRemote) {
424
const existing = result.get(server.name);
425
if (existing) {
426
this.logService.warn(localize('overwriting', "Overwriting mcp server '{0}' from {1} with {2}.", server.name, server.mcpResource.path, existing.mcpResource.path));
427
}
428
result.set(server.name, server);
429
}
430
431
for (const server of workspace) {
432
const existing = result.get(server.name);
433
if (existing) {
434
this.logService.warn(localize('overwriting', "Overwriting mcp server '{0}' from {1} with {2}.", server.name, server.mcpResource.path, existing.mcpResource.path));
435
}
436
result.set(server.name, server);
437
}
438
439
return [...result.values()];
440
}
441
442
canInstall(mcpServer: IWorkbenchMcpServer): true | IMarkdownString {
443
if (!(mcpServer instanceof McpWorkbenchServer)) {
444
return new MarkdownString().appendText(localize('not an extension', "The provided object is not an mcp server."));
445
}
446
447
if (mcpServer.gallery) {
448
const result = this.mcpManagementService.canInstall(mcpServer.gallery);
449
if (result === true) {
450
return true;
451
}
452
453
return result;
454
}
455
456
if (mcpServer.installable) {
457
const result = this.mcpManagementService.canInstall(mcpServer.installable);
458
if (result === true) {
459
return true;
460
}
461
462
return result;
463
}
464
465
466
return new MarkdownString().appendText(localize('cannot be installed', "Cannot install the '{0}' MCP Server because it is not available in this setup.", mcpServer.label));
467
}
468
469
async install(server: IWorkbenchMcpServer, installOptions?: IWorkbencMcpServerInstallOptions): Promise<IWorkbenchMcpServer> {
470
if (!(server instanceof McpWorkbenchServer)) {
471
throw new Error('Invalid server instance');
472
}
473
474
if (server.installable) {
475
const installable = server.installable;
476
return this.doInstall(server, () => this.mcpManagementService.install(installable, installOptions));
477
}
478
479
if (server.gallery) {
480
const gallery = server.gallery;
481
return this.doInstall(server, () => this.mcpManagementService.installFromGallery(gallery, installOptions));
482
}
483
484
throw new Error('No installable server found');
485
}
486
487
async uninstall(server: IWorkbenchMcpServer): Promise<void> {
488
if (!server.local) {
489
throw new Error('Local server is missing');
490
}
491
await this.mcpManagementService.uninstall(server.local);
492
}
493
494
private async doInstall(server: McpWorkbenchServer, installTask: () => Promise<IWorkbenchLocalMcpServer>): Promise<IWorkbenchMcpServer> {
495
const source = server.gallery ? 'gallery' : 'local';
496
const serverName = server.name;
497
// Check for inputs in installable config or if it comes from handleURL with inputs
498
const hasInputs = !!(server.installable?.inputs && server.installable.inputs.length > 0);
499
500
this.installing.push(server);
501
this._onChange.fire(server);
502
503
try {
504
await installTask();
505
const result = await this.waitAndGetInstalledMcpServer(server);
506
507
// Track successful installation
508
this.telemetryService.publicLog2<McpServerInstallData, McpServerInstallClassification>('mcp/serverInstall', {
509
serverName,
510
source,
511
scope: result.local?.scope ?? 'unknown',
512
success: true,
513
hasInputs
514
});
515
516
return result;
517
} catch (error) {
518
// Track failed installation
519
this.telemetryService.publicLog2<McpServerInstallData, McpServerInstallClassification>('mcp/serverInstall', {
520
serverName,
521
source,
522
scope: 'unknown',
523
success: false,
524
error: error instanceof Error ? error.message : String(error),
525
hasInputs
526
});
527
528
throw error;
529
} finally {
530
if (this.installing.includes(server)) {
531
this.installing.splice(this.installing.indexOf(server), 1);
532
this._onChange.fire(server);
533
}
534
}
535
}
536
537
private async waitAndGetInstalledMcpServer(server: McpWorkbenchServer): Promise<IWorkbenchMcpServer> {
538
let installed = this.local.find(local => local.name === server.name);
539
if (!installed) {
540
await Event.toPromise(Event.filter(this.onChange, e => !!e && this.local.some(local => local.name === server.name)));
541
}
542
installed = this.local.find(local => local.name === server.name);
543
if (!installed) {
544
// This should not happen
545
throw new Error('Extension should have been installed');
546
}
547
return installed;
548
}
549
550
getMcpConfigPath(localMcpServer: IWorkbenchLocalMcpServer): IMcpConfigPath | undefined;
551
getMcpConfigPath(mcpResource: URI): Promise<IMcpConfigPath | undefined>;
552
getMcpConfigPath(arg: URI | IWorkbenchLocalMcpServer): Promise<IMcpConfigPath | undefined> | IMcpConfigPath | undefined {
553
if (arg instanceof URI) {
554
const mcpResource = arg;
555
for (const profile of this.userDataProfilesService.profiles) {
556
if (this.uriIdentityService.extUri.isEqual(profile.mcpResource, mcpResource)) {
557
return this.getUserMcpConfigPath(mcpResource);
558
}
559
}
560
561
return this.remoteAgentService.getEnvironment().then(remoteEnvironment => {
562
if (remoteEnvironment && this.uriIdentityService.extUri.isEqual(remoteEnvironment.mcpResource, mcpResource)) {
563
return this.getRemoteMcpConfigPath(mcpResource);
564
}
565
return this.getWorkspaceMcpConfigPath(mcpResource);
566
});
567
}
568
569
if (arg.scope === LocalMcpServerScope.User) {
570
return this.getUserMcpConfigPath(arg.mcpResource);
571
}
572
573
if (arg.scope === LocalMcpServerScope.Workspace) {
574
return this.getWorkspaceMcpConfigPath(arg.mcpResource);
575
}
576
577
if (arg.scope === LocalMcpServerScope.RemoteUser) {
578
return this.getRemoteMcpConfigPath(arg.mcpResource);
579
}
580
581
return undefined;
582
}
583
584
private getUserMcpConfigPath(mcpResource: URI): IMcpConfigPath {
585
return {
586
id: USER_CONFIG_ID,
587
key: 'userLocalValue',
588
target: ConfigurationTarget.USER_LOCAL,
589
label: localize('mcp.configuration.userLocalValue', 'Global in {0}', this.productService.nameShort),
590
scope: StorageScope.PROFILE,
591
order: McpCollectionSortOrder.User,
592
uri: mcpResource,
593
section: [],
594
};
595
}
596
597
private getRemoteMcpConfigPath(mcpResource: URI): IMcpConfigPath {
598
return {
599
id: REMOTE_USER_CONFIG_ID,
600
key: 'userRemoteValue',
601
target: ConfigurationTarget.USER_REMOTE,
602
label: this.environmentService.remoteAuthority ? this.labelService.getHostLabel(Schemas.vscodeRemote, this.environmentService.remoteAuthority) : 'Remote',
603
scope: StorageScope.PROFILE,
604
order: McpCollectionSortOrder.User + McpCollectionSortOrder.RemoteBoost,
605
remoteAuthority: this.environmentService.remoteAuthority,
606
uri: mcpResource,
607
section: [],
608
};
609
}
610
611
private getWorkspaceMcpConfigPath(mcpResource: URI): IMcpConfigPath | undefined {
612
const workspace = this.workspaceService.getWorkspace();
613
if (workspace.configuration && this.uriIdentityService.extUri.isEqual(workspace.configuration, mcpResource)) {
614
return {
615
id: WORKSPACE_CONFIG_ID,
616
key: 'workspaceValue',
617
target: ConfigurationTarget.WORKSPACE,
618
label: basename(mcpResource),
619
scope: StorageScope.WORKSPACE,
620
order: McpCollectionSortOrder.Workspace,
621
remoteAuthority: this.environmentService.remoteAuthority,
622
uri: mcpResource,
623
section: ['settings', mcpConfigurationSection],
624
};
625
}
626
627
const workspaceFolders = workspace.folders;
628
for (let index = 0; index < workspaceFolders.length; index++) {
629
const workspaceFolder = workspaceFolders[index];
630
if (this.uriIdentityService.extUri.isEqual(this.uriIdentityService.extUri.joinPath(workspaceFolder.uri, WORKSPACE_STANDALONE_CONFIGURATIONS[MCP_CONFIGURATION_KEY]), mcpResource)) {
631
return {
632
id: `${WORKSPACE_FOLDER_CONFIG_ID_PREFIX}${index}`,
633
key: 'workspaceFolderValue',
634
target: ConfigurationTarget.WORKSPACE_FOLDER,
635
label: `${workspaceFolder.name}/.vscode/mcp.json`,
636
scope: StorageScope.WORKSPACE,
637
remoteAuthority: this.environmentService.remoteAuthority,
638
order: McpCollectionSortOrder.WorkspaceFolder,
639
uri: mcpResource,
640
workspaceFolder,
641
};
642
}
643
}
644
645
return undefined;
646
}
647
648
async handleURL(uri: URI): Promise<boolean> {
649
if (uri.path === 'mcp/install') {
650
return this.handleMcpInstallUri(uri);
651
}
652
if (uri.path.startsWith('mcp/by-name/')) {
653
const mcpServerName = uri.path.substring('mcp/by-name/'.length);
654
if (mcpServerName) {
655
return this.handleMcpServerByName(mcpServerName);
656
}
657
}
658
if (uri.path.startsWith('mcp/')) {
659
const mcpServerUrl = uri.path.substring(4);
660
if (mcpServerUrl) {
661
return this.handleMcpServerUrl(`${Schemas.https}://${mcpServerUrl}`);
662
}
663
}
664
return false;
665
}
666
667
private async handleMcpInstallUri(uri: URI): Promise<boolean> {
668
let parsed: IMcpServerConfiguration & { name: string; inputs?: IMcpServerVariable[]; gallery?: boolean };
669
try {
670
parsed = JSON.parse(decodeURIComponent(uri.query));
671
} catch (e) {
672
return false;
673
}
674
675
try {
676
const { name, inputs, gallery, ...config } = parsed;
677
if (config.type === undefined) {
678
(<Mutable<IMcpServerConfiguration>>config).type = (<IMcpStdioServerConfiguration>parsed).command ? McpServerType.LOCAL : McpServerType.REMOTE;
679
}
680
this.open(this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, undefined, { name, config, inputs }));
681
} catch (e) {
682
// ignore
683
}
684
return true;
685
}
686
687
private async handleMcpServerUrl(url: string): Promise<boolean> {
688
try {
689
const gallery = await this.mcpGalleryService.getMcpServer(url);
690
if (!gallery) {
691
this.logService.info(`MCP server '${url}' not found`);
692
return true;
693
}
694
const local = this.local.find(e => e.name === gallery.name) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, gallery, undefined);
695
this.open(local);
696
} catch (e) {
697
// ignore
698
this.logService.error(e);
699
}
700
return true;
701
}
702
703
private async handleMcpServerByName(name: string): Promise<boolean> {
704
try {
705
const [gallery] = await this.mcpGalleryService.getMcpServersFromGallery([{ name }]);
706
if (!gallery) {
707
this.logService.info(`MCP server '${name}' not found`);
708
return true;
709
}
710
const local = this.local.find(e => e.name === gallery.name) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, gallery, undefined);
711
this.open(local);
712
} catch (e) {
713
// ignore
714
this.logService.error(e);
715
}
716
return true;
717
}
718
719
async openSearch(searchValue: string, preserveFoucs?: boolean): Promise<void> {
720
await this.extensionsWorkbenchService.openSearch(`@mcp ${searchValue}`, preserveFoucs);
721
}
722
723
async open(extension: IWorkbenchMcpServer, options?: IEditorOptions): Promise<void> {
724
await this.editorService.openEditor(this.instantiationService.createInstance(McpServerEditorInput, extension), options, MODAL_GROUP);
725
}
726
727
private getInstallState(extension: McpWorkbenchServer): McpServerInstallState {
728
if (this.installing.some(i => i.name === extension.name)) {
729
return McpServerInstallState.Installing;
730
}
731
if (this.uninstalling.some(e => e.name === extension.name)) {
732
return McpServerInstallState.Uninstalling;
733
}
734
const local = this.local.find(e => e === extension);
735
return local ? McpServerInstallState.Installed : McpServerInstallState.Uninstalled;
736
}
737
738
private getRuntimeStatus(mcpServer: McpWorkbenchServer): McpServerEnablementStatus | undefined {
739
const enablementStatus = this.getEnablementStatus(mcpServer);
740
741
if (enablementStatus) {
742
return enablementStatus;
743
}
744
745
if (!this.mcpService.servers.get().find(s => s.definition.id === mcpServer.id)) {
746
return { state: McpServerEnablementState.Disabled };
747
}
748
749
return undefined;
750
}
751
752
private getEnablementStatus(mcpServer: McpWorkbenchServer): McpServerEnablementStatus | undefined {
753
if (!mcpServer.local) {
754
return undefined;
755
}
756
757
const settingsCommandLink = createCommandUri('workbench.action.openSettings', { query: `@id:${mcpAccessConfig}` }).toString();
758
const accessValue = this.configurationService.getValue(mcpAccessConfig);
759
760
if (accessValue === McpAccessValue.None) {
761
return {
762
state: McpServerEnablementState.DisabledByAccess,
763
message: {
764
severity: Severity.Warning,
765
text: new MarkdownString(localize('disabled - all not allowed', "This MCP Server is disabled because MCP servers are configured to be disabled in the Editor. Please check your [settings]({0}).", settingsCommandLink))
766
}
767
};
768
769
}
770
771
if (accessValue === McpAccessValue.Registry) {
772
if (!mcpServer.gallery) {
773
return {
774
state: McpServerEnablementState.DisabledByAccess,
775
message: {
776
severity: Severity.Warning,
777
text: new MarkdownString(localize('disabled - some not allowed', "This MCP Server is disabled because it is configured to be disabled in the Editor. Please check your [settings]({0}).", settingsCommandLink))
778
}
779
};
780
}
781
782
const remoteUrl = mcpServer.local.config.type === McpServerType.REMOTE && mcpServer.local.config.url;
783
if (remoteUrl && !mcpServer.gallery.configuration.remotes?.some(remote => remote.url === remoteUrl)) {
784
return {
785
state: McpServerEnablementState.DisabledByAccess,
786
message: {
787
severity: Severity.Warning,
788
text: new MarkdownString(localize('disabled - some not allowed', "This MCP Server is disabled because it is configured to be disabled in the Editor. Please check your [settings]({0}).", settingsCommandLink))
789
}
790
};
791
}
792
}
793
794
return undefined;
795
}
796
797
}
798
799
export class MCPContextsInitialisation extends Disposable implements IWorkbenchContribution {
800
801
static ID = 'workbench.mcp.contexts.initialisation';
802
803
constructor(
804
@IMcpWorkbenchService mcpWorkbenchService: IMcpWorkbenchService,
805
@IMcpGalleryManifestService mcpGalleryManifestService: IMcpGalleryManifestService,
806
@IContextKeyService contextKeyService: IContextKeyService,
807
) {
808
super();
809
810
const mcpServersGalleryStatus = McpServersGalleryStatusContext.bindTo(contextKeyService);
811
mcpServersGalleryStatus.set(mcpGalleryManifestService.mcpGalleryManifestStatus);
812
this._register(mcpGalleryManifestService.onDidChangeMcpGalleryManifestStatus(status => mcpServersGalleryStatus.set(status)));
813
814
const hasInstalledMcpServersContextKey = HasInstalledMcpServersContext.bindTo(contextKeyService);
815
mcpWorkbenchService.queryLocal().finally(() => {
816
hasInstalledMcpServersContextKey.set(mcpWorkbenchService.local.length > 0);
817
this._register(mcpWorkbenchService.onChange(() => hasInstalledMcpServersContextKey.set(mcpWorkbenchService.local.length > 0)));
818
});
819
}
820
}
821
822