Path: blob/main/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts
5257 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CancellationToken } from '../../../../base/common/cancellation.js';6import { Emitter, Event } from '../../../../base/common/event.js';7import { createCommandUri, IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';8import { Disposable } from '../../../../base/common/lifecycle.js';9import { Schemas } from '../../../../base/common/network.js';10import { basename } from '../../../../base/common/resources.js';11import { Mutable } from '../../../../base/common/types.js';12import { URI } from '../../../../base/common/uri.js';13import { localize } from '../../../../nls.js';14import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';15import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';16import { IEditorOptions } from '../../../../platform/editor/common/editor.js';17import { IFileService } from '../../../../platform/files/common/files.js';18import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';19import { ILabelService } from '../../../../platform/label/common/label.js';20import { ILogService } from '../../../../platform/log/common/log.js';21import { IGalleryMcpServer, IMcpGalleryService, IQueryOptions, IInstallableMcpServer, IGalleryMcpServerConfiguration, mcpAccessConfig, McpAccessValue, IAllowedMcpServersService } from '../../../../platform/mcp/common/mcpManagement.js';22import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';23import { IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration, McpServerType } from '../../../../platform/mcp/common/mcpPlatformTypes.js';24import { IProductService } from '../../../../platform/product/common/productService.js';25import { StorageScope } from '../../../../platform/storage/common/storage.js';26import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';27import { IURLService } from '../../../../platform/url/common/url.js';28import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';29import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';30import { IWorkbenchContribution } from '../../../common/contributions.js';31import { MCP_CONFIGURATION_KEY, WORKSPACE_STANDALONE_CONFIGURATIONS } from '../../../services/configuration/common/configuration.js';32import { IEditorService, MODAL_GROUP } from '../../../services/editor/common/editorService.js';33import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';34import { 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';35import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';36import { mcpConfigurationSection } from '../common/mcpConfiguration.js';37import { McpServerInstallData, McpServerInstallClassification } from '../common/mcpServer.js';38import { HasInstalledMcpServersContext, IMcpConfigPath, IMcpService, IMcpWorkbenchService, IWorkbenchMcpServer, McpCollectionSortOrder, McpServerEnablementState, McpServerInstallState, McpServerEnablementStatus, McpServersGalleryStatusContext } from '../common/mcpTypes.js';39import { McpServerEditorInput } from './mcpServerEditorInput.js';40import { IMcpGalleryManifestService } from '../../../../platform/mcp/common/mcpGalleryManifest.js';41import { IIterativePager, IIterativePage } from '../../../../base/common/paging.js';42import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';43import { runOnChange } from '../../../../base/common/observable.js';44import Severity from '../../../../base/common/severity.js';45import { Queue } from '../../../../base/common/async.js';4647interface IMcpServerStateProvider<T> {48(mcpWorkbenchServer: McpWorkbenchServer): T;49}5051class McpWorkbenchServer implements IWorkbenchMcpServer {5253constructor(54private installStateProvider: IMcpServerStateProvider<McpServerInstallState>,55private runtimeStateProvider: IMcpServerStateProvider<McpServerEnablementStatus | undefined>,56public local: IWorkbenchLocalMcpServer | undefined,57public gallery: IGalleryMcpServer | undefined,58public readonly installable: IInstallableMcpServer | undefined,59@IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService,60@IFileService private readonly fileService: IFileService,61) {62this.local = local;63}6465get id(): string {66return this.local?.id ?? this.gallery?.name ?? this.installable?.name ?? this.name;67}6869get name(): string {70return this.gallery?.name ?? this.local?.name ?? this.installable?.name ?? '';71}7273get label(): string {74return this.gallery?.displayName ?? this.local?.displayName ?? this.local?.name ?? this.installable?.name ?? '';75}7677get icon(): {78readonly dark: string;79readonly light: string;80} | undefined {81return this.gallery?.icon ?? this.local?.icon;82}8384get installState(): McpServerInstallState {85return this.installStateProvider(this);86}8788get codicon(): string | undefined {89return this.gallery?.codicon ?? this.local?.codicon;90}9192get publisherDisplayName(): string | undefined {93return this.gallery?.publisherDisplayName ?? this.local?.publisherDisplayName ?? this.gallery?.publisher ?? this.local?.publisher;94}9596get publisherUrl(): string | undefined {97return this.gallery?.publisherDomain?.link;98}99100get description(): string {101return this.gallery?.description ?? this.local?.description ?? '';102}103104get starsCount(): number {105return this.gallery?.starsCount ?? 0;106}107108get license(): string | undefined {109return this.gallery?.license;110}111112get repository(): string | undefined {113return this.gallery?.repositoryUrl;114}115116get config(): IMcpServerConfiguration | undefined {117return this.local?.config ?? this.installable?.config;118}119120get runtimeStatus(): McpServerEnablementStatus | undefined {121return this.runtimeStateProvider(this);122}123124get readmeUrl(): URI | undefined {125return this.local?.readmeUrl ?? (this.gallery?.readmeUrl ? URI.parse(this.gallery.readmeUrl) : undefined);126}127128async getReadme(token: CancellationToken): Promise<string> {129if (this.local?.readmeUrl) {130const content = await this.fileService.readFile(this.local.readmeUrl);131return content.value.toString();132}133134if (this.gallery?.readme) {135return this.gallery.readme;136}137138if (this.gallery?.readmeUrl) {139return this.mcpGalleryService.getReadme(this.gallery, token);140}141142return Promise.reject(new Error('not available'));143}144145async getManifest(token: CancellationToken): Promise<IGalleryMcpServerConfiguration> {146if (this.local?.manifest) {147return this.local.manifest;148}149150if (this.gallery) {151return this.gallery.configuration;152}153154throw new Error('No manifest available');155}156157}158159export class McpWorkbenchService extends Disposable implements IMcpWorkbenchService {160161_serviceBrand: undefined;162163private installing: McpWorkbenchServer[] = [];164private uninstalling: McpWorkbenchServer[] = [];165166private _local: McpWorkbenchServer[] = [];167get local(): readonly McpWorkbenchServer[] { return [...this._local]; }168169private readonly _onChange = this._register(new Emitter<IWorkbenchMcpServer | undefined>());170readonly onChange = this._onChange.event;171172private readonly _onReset = this._register(new Emitter<void>());173readonly onReset = this._onReset.event;174175constructor(176@IMcpGalleryManifestService mcpGalleryManifestService: IMcpGalleryManifestService,177@IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService,178@IWorkbenchMcpManagementService private readonly mcpManagementService: IWorkbenchMcpManagementService,179@IEditorService private readonly editorService: IEditorService,180@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,181@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,182@IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService,183@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,184@ILabelService private readonly labelService: ILabelService,185@IProductService private readonly productService: IProductService,186@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,187@IConfigurationService private readonly configurationService: IConfigurationService,188@IInstantiationService private readonly instantiationService: IInstantiationService,189@ITelemetryService private readonly telemetryService: ITelemetryService,190@ILogService private readonly logService: ILogService,191@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,192@IAllowedMcpServersService private readonly allowedMcpServersService: IAllowedMcpServersService,193@IMcpService private readonly mcpService: IMcpService,194@IURLService urlService: IURLService,195) {196super();197this._register(this.mcpManagementService.onDidInstallMcpServersInCurrentProfile(e => this.onDidInstallMcpServers(e)));198this._register(this.mcpManagementService.onDidUpdateMcpServersInCurrentProfile(e => this.onDidUpdateMcpServers(e)));199this._register(this.mcpManagementService.onDidUninstallMcpServerInCurrentProfile(e => this.onDidUninstallMcpServer(e)));200this._register(this.mcpManagementService.onDidChangeProfile(e => this.onDidChangeProfile()));201this.queryLocal().then(() => {202if (this._store.isDisposed) {203return;204}205const queue = this._register(new Queue());206this._register(mcpGalleryManifestService.onDidChangeMcpGalleryManifest(e => queue.queue(() => this.syncInstalledMcpServers())));207queue.queue(() => this.syncInstalledMcpServers());208});209urlService.registerHandler(this);210this._register(this.configurationService.onDidChangeConfiguration(e => {211if (e.affectsConfiguration(mcpAccessConfig)) {212this._onChange.fire(undefined);213}214}));215this._register(this.allowedMcpServersService.onDidChangeAllowedMcpServers(() => {216this._local = this.sort(this._local);217this._onChange.fire(undefined);218}));219this._register(runOnChange(mcpService.servers, () => {220this._local = this.sort(this._local);221this._onChange.fire(undefined);222}));223}224225private async onDidChangeProfile() {226await this.queryLocal();227this._onChange.fire(undefined);228this._onReset.fire();229}230231private areSameMcpServers(a: { name: string; scope: LocalMcpServerScope } | undefined, b: { name: string; scope: LocalMcpServerScope } | undefined): boolean {232if (a === b) {233return true;234}235if (!a || !b) {236return false;237}238return a.name === b.name && a.scope === b.scope;239}240241private onDidUninstallMcpServer(e: DidUninstallWorkbenchMcpServerEvent) {242if (e.error) {243return;244}245const uninstalled = this._local.find(server => this.areSameMcpServers(server.local, e));246if (uninstalled) {247this._local = this._local.filter(server => server !== uninstalled);248this._onChange.fire(uninstalled);249}250}251252private onDidInstallMcpServers(e: readonly IWorkbenchMcpServerInstallResult[]) {253const servers: IWorkbenchMcpServer[] = [];254for (const { local, source, name } of e) {255let server = this.installing.find(server => server.local && local ? this.areSameMcpServers(server.local, local) : server.name === name);256this.installing = server ? this.installing.filter(e => e !== server) : this.installing;257if (local) {258if (server) {259server.local = local;260} else {261server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), local, source, undefined);262}263if (!local.galleryUrl) {264server.gallery = undefined;265}266this._local = this._local.filter(server => !this.areSameMcpServers(server.local, local));267this.addServer(server);268}269this._onChange.fire(server);270}271if (servers.some(server => server.local?.galleryUrl && !server.gallery)) {272this.syncInstalledMcpServers();273}274}275276private onDidUpdateMcpServers(e: readonly IWorkbenchMcpServerInstallResult[]) {277for (const result of e) {278if (!result.local) {279continue;280}281const serverIndex = this._local.findIndex(server => this.areSameMcpServers(server.local, result.local));282let server: McpWorkbenchServer;283if (serverIndex !== -1) {284this._local[serverIndex].local = result.local;285server = this._local[serverIndex];286} else {287server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), result.local, result.source, undefined);288this.addServer(server);289}290this._onChange.fire(server);291}292}293294private fromGallery(gallery: IGalleryMcpServer): IWorkbenchMcpServer | undefined {295for (const local of this._local) {296if (local.name === gallery.name) {297local.gallery = gallery;298return local;299}300}301return undefined;302}303304private async syncInstalledMcpServers(): Promise<void> {305const infos: { name: string; id?: string }[] = [];306307for (const installed of this.local) {308if (installed.local?.source !== 'gallery') {309continue;310}311if (installed.local.galleryUrl) {312infos.push({ name: installed.local.name, id: installed.local.galleryId });313}314}315316if (infos.length) {317const galleryServers = await this.mcpGalleryService.getMcpServersFromGallery(infos);318await this.syncInstalledMcpServersWithGallery(galleryServers);319}320}321322private async syncInstalledMcpServersWithGallery(gallery: IGalleryMcpServer[]): Promise<void> {323const galleryMap = new Map<string, IGalleryMcpServer>(gallery.map(server => [server.name, server]));324for (const mcpServer of this.local) {325if (!mcpServer.local) {326continue;327}328const key = mcpServer.local.name;329const gallery = key ? galleryMap.get(key) : undefined;330331if (!gallery || gallery.galleryUrl !== mcpServer.local.galleryUrl) {332if (mcpServer.gallery) {333mcpServer.gallery = undefined;334this._onChange.fire(mcpServer);335}336continue;337}338339mcpServer.gallery = gallery;340if (!mcpServer.local.manifest) {341mcpServer.local = await this.mcpManagementService.updateMetadata(mcpServer.local, gallery);342}343this._onChange.fire(mcpServer);344}345}346347async queryGallery(options?: IQueryOptions, token?: CancellationToken): Promise<IIterativePager<IWorkbenchMcpServer>> {348if (!this.mcpGalleryService.isEnabled()) {349return {350firstPage: { items: [], hasMore: false },351getNextPage: async () => ({ items: [], hasMore: false })352};353}354const pager = await this.mcpGalleryService.query(options, token);355356const mapPage = (page: IIterativePage<IGalleryMcpServer>): IIterativePage<IWorkbenchMcpServer> => ({357items: page.items.map(gallery => this.fromGallery(gallery) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, gallery, undefined)),358hasMore: page.hasMore359});360361return {362firstPage: mapPage(pager.firstPage),363getNextPage: async (ct) => {364const nextPage = await pager.getNextPage(ct);365return mapPage(nextPage);366}367};368}369370async queryLocal(): Promise<IWorkbenchMcpServer[]> {371const installed = await this.mcpManagementService.getInstalled();372this._local = this.sort(installed.map(i => {373const existing = this._local.find(local => local.id === i.id);374const local = existing ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, undefined, undefined);375local.local = i;376return local;377}));378this._onChange.fire(undefined);379return [...this.local];380}381382private addServer(server: McpWorkbenchServer): void {383this._local.push(server);384this._local = this.sort(this._local);385}386387private sort(local: McpWorkbenchServer[]): McpWorkbenchServer[] {388return local.sort((a, b) => {389if (a.name === b.name) {390if (!a.runtimeStatus || a.runtimeStatus.state === McpServerEnablementState.Enabled) {391return -1;392}393if (!b.runtimeStatus || b.runtimeStatus.state === McpServerEnablementState.Enabled) {394return 1;395}396return 0;397}398return a.name.localeCompare(b.name);399});400}401402getEnabledLocalMcpServers(): IWorkbenchLocalMcpServer[] {403const result = new Map<string, IWorkbenchLocalMcpServer>();404const userRemote: IWorkbenchLocalMcpServer[] = [];405const workspace: IWorkbenchLocalMcpServer[] = [];406407for (const server of this.local) {408const enablementStatus = this.getEnablementStatus(server);409if (enablementStatus && enablementStatus.state !== McpServerEnablementState.Enabled) {410continue;411}412413if (server.local?.scope === LocalMcpServerScope.User) {414result.set(server.name, server.local);415} else if (server.local?.scope === LocalMcpServerScope.RemoteUser) {416userRemote.push(server.local);417} else if (server.local?.scope === LocalMcpServerScope.Workspace) {418workspace.push(server.local);419}420}421422for (const server of userRemote) {423const existing = result.get(server.name);424if (existing) {425this.logService.warn(localize('overwriting', "Overwriting mcp server '{0}' from {1} with {2}.", server.name, server.mcpResource.path, existing.mcpResource.path));426}427result.set(server.name, server);428}429430for (const server of workspace) {431const existing = result.get(server.name);432if (existing) {433this.logService.warn(localize('overwriting', "Overwriting mcp server '{0}' from {1} with {2}.", server.name, server.mcpResource.path, existing.mcpResource.path));434}435result.set(server.name, server);436}437438return [...result.values()];439}440441canInstall(mcpServer: IWorkbenchMcpServer): true | IMarkdownString {442if (!(mcpServer instanceof McpWorkbenchServer)) {443return new MarkdownString().appendText(localize('not an extension', "The provided object is not an mcp server."));444}445446if (mcpServer.gallery) {447const result = this.mcpManagementService.canInstall(mcpServer.gallery);448if (result === true) {449return true;450}451452return result;453}454455if (mcpServer.installable) {456const result = this.mcpManagementService.canInstall(mcpServer.installable);457if (result === true) {458return true;459}460461return result;462}463464465return new MarkdownString().appendText(localize('cannot be installed', "Cannot install the '{0}' MCP Server because it is not available in this setup.", mcpServer.label));466}467468async install(server: IWorkbenchMcpServer, installOptions?: IWorkbencMcpServerInstallOptions): Promise<IWorkbenchMcpServer> {469if (!(server instanceof McpWorkbenchServer)) {470throw new Error('Invalid server instance');471}472473if (server.installable) {474const installable = server.installable;475return this.doInstall(server, () => this.mcpManagementService.install(installable, installOptions));476}477478if (server.gallery) {479const gallery = server.gallery;480return this.doInstall(server, () => this.mcpManagementService.installFromGallery(gallery, installOptions));481}482483throw new Error('No installable server found');484}485486async uninstall(server: IWorkbenchMcpServer): Promise<void> {487if (!server.local) {488throw new Error('Local server is missing');489}490await this.mcpManagementService.uninstall(server.local);491}492493private async doInstall(server: McpWorkbenchServer, installTask: () => Promise<IWorkbenchLocalMcpServer>): Promise<IWorkbenchMcpServer> {494const source = server.gallery ? 'gallery' : 'local';495const serverName = server.name;496// Check for inputs in installable config or if it comes from handleURL with inputs497const hasInputs = !!(server.installable?.inputs && server.installable.inputs.length > 0);498499this.installing.push(server);500this._onChange.fire(server);501502try {503await installTask();504const result = await this.waitAndGetInstalledMcpServer(server);505506// Track successful installation507this.telemetryService.publicLog2<McpServerInstallData, McpServerInstallClassification>('mcp/serverInstall', {508serverName,509source,510scope: result.local?.scope ?? 'unknown',511success: true,512hasInputs513});514515return result;516} catch (error) {517// Track failed installation518this.telemetryService.publicLog2<McpServerInstallData, McpServerInstallClassification>('mcp/serverInstall', {519serverName,520source,521scope: 'unknown',522success: false,523error: error instanceof Error ? error.message : String(error),524hasInputs525});526527throw error;528} finally {529if (this.installing.includes(server)) {530this.installing.splice(this.installing.indexOf(server), 1);531this._onChange.fire(server);532}533}534}535536private async waitAndGetInstalledMcpServer(server: McpWorkbenchServer): Promise<IWorkbenchMcpServer> {537let installed = this.local.find(local => local.name === server.name);538if (!installed) {539await Event.toPromise(Event.filter(this.onChange, e => !!e && this.local.some(local => local.name === server.name)));540}541installed = this.local.find(local => local.name === server.name);542if (!installed) {543// This should not happen544throw new Error('Extension should have been installed');545}546return installed;547}548549getMcpConfigPath(localMcpServer: IWorkbenchLocalMcpServer): IMcpConfigPath | undefined;550getMcpConfigPath(mcpResource: URI): Promise<IMcpConfigPath | undefined>;551getMcpConfigPath(arg: URI | IWorkbenchLocalMcpServer): Promise<IMcpConfigPath | undefined> | IMcpConfigPath | undefined {552if (arg instanceof URI) {553const mcpResource = arg;554for (const profile of this.userDataProfilesService.profiles) {555if (this.uriIdentityService.extUri.isEqual(profile.mcpResource, mcpResource)) {556return this.getUserMcpConfigPath(mcpResource);557}558}559560return this.remoteAgentService.getEnvironment().then(remoteEnvironment => {561if (remoteEnvironment && this.uriIdentityService.extUri.isEqual(remoteEnvironment.mcpResource, mcpResource)) {562return this.getRemoteMcpConfigPath(mcpResource);563}564return this.getWorkspaceMcpConfigPath(mcpResource);565});566}567568if (arg.scope === LocalMcpServerScope.User) {569return this.getUserMcpConfigPath(arg.mcpResource);570}571572if (arg.scope === LocalMcpServerScope.Workspace) {573return this.getWorkspaceMcpConfigPath(arg.mcpResource);574}575576if (arg.scope === LocalMcpServerScope.RemoteUser) {577return this.getRemoteMcpConfigPath(arg.mcpResource);578}579580return undefined;581}582583private getUserMcpConfigPath(mcpResource: URI): IMcpConfigPath {584return {585id: USER_CONFIG_ID,586key: 'userLocalValue',587target: ConfigurationTarget.USER_LOCAL,588label: localize('mcp.configuration.userLocalValue', 'Global in {0}', this.productService.nameShort),589scope: StorageScope.PROFILE,590order: McpCollectionSortOrder.User,591uri: mcpResource,592section: [],593};594}595596private getRemoteMcpConfigPath(mcpResource: URI): IMcpConfigPath {597return {598id: REMOTE_USER_CONFIG_ID,599key: 'userRemoteValue',600target: ConfigurationTarget.USER_REMOTE,601label: this.environmentService.remoteAuthority ? this.labelService.getHostLabel(Schemas.vscodeRemote, this.environmentService.remoteAuthority) : 'Remote',602scope: StorageScope.PROFILE,603order: McpCollectionSortOrder.User + McpCollectionSortOrder.RemoteBoost,604remoteAuthority: this.environmentService.remoteAuthority,605uri: mcpResource,606section: [],607};608}609610private getWorkspaceMcpConfigPath(mcpResource: URI): IMcpConfigPath | undefined {611const workspace = this.workspaceService.getWorkspace();612if (workspace.configuration && this.uriIdentityService.extUri.isEqual(workspace.configuration, mcpResource)) {613return {614id: WORKSPACE_CONFIG_ID,615key: 'workspaceValue',616target: ConfigurationTarget.WORKSPACE,617label: basename(mcpResource),618scope: StorageScope.WORKSPACE,619order: McpCollectionSortOrder.Workspace,620remoteAuthority: this.environmentService.remoteAuthority,621uri: mcpResource,622section: ['settings', mcpConfigurationSection],623};624}625626const workspaceFolders = workspace.folders;627for (let index = 0; index < workspaceFolders.length; index++) {628const workspaceFolder = workspaceFolders[index];629if (this.uriIdentityService.extUri.isEqual(this.uriIdentityService.extUri.joinPath(workspaceFolder.uri, WORKSPACE_STANDALONE_CONFIGURATIONS[MCP_CONFIGURATION_KEY]), mcpResource)) {630return {631id: `${WORKSPACE_FOLDER_CONFIG_ID_PREFIX}${index}`,632key: 'workspaceFolderValue',633target: ConfigurationTarget.WORKSPACE_FOLDER,634label: `${workspaceFolder.name}/.vscode/mcp.json`,635scope: StorageScope.WORKSPACE,636remoteAuthority: this.environmentService.remoteAuthority,637order: McpCollectionSortOrder.WorkspaceFolder,638uri: mcpResource,639workspaceFolder,640};641}642}643644return undefined;645}646647async handleURL(uri: URI): Promise<boolean> {648if (uri.path === 'mcp/install') {649return this.handleMcpInstallUri(uri);650}651if (uri.path.startsWith('mcp/by-name/')) {652const mcpServerName = uri.path.substring('mcp/by-name/'.length);653if (mcpServerName) {654return this.handleMcpServerByName(mcpServerName);655}656}657if (uri.path.startsWith('mcp/')) {658const mcpServerUrl = uri.path.substring(4);659if (mcpServerUrl) {660return this.handleMcpServerUrl(`${Schemas.https}://${mcpServerUrl}`);661}662}663return false;664}665666private async handleMcpInstallUri(uri: URI): Promise<boolean> {667let parsed: IMcpServerConfiguration & { name: string; inputs?: IMcpServerVariable[]; gallery?: boolean };668try {669parsed = JSON.parse(decodeURIComponent(uri.query));670} catch (e) {671return false;672}673674try {675const { name, inputs, gallery, ...config } = parsed;676if (config.type === undefined) {677(<Mutable<IMcpServerConfiguration>>config).type = (<IMcpStdioServerConfiguration>parsed).command ? McpServerType.LOCAL : McpServerType.REMOTE;678}679this.open(this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, undefined, { name, config, inputs }));680} catch (e) {681// ignore682}683return true;684}685686private async handleMcpServerUrl(url: string): Promise<boolean> {687try {688const gallery = await this.mcpGalleryService.getMcpServer(url);689if (!gallery) {690this.logService.info(`MCP server '${url}' not found`);691return true;692}693const local = this.local.find(e => e.name === gallery.name) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, gallery, undefined);694this.open(local);695} catch (e) {696// ignore697this.logService.error(e);698}699return true;700}701702private async handleMcpServerByName(name: string): Promise<boolean> {703try {704const [gallery] = await this.mcpGalleryService.getMcpServersFromGallery([{ name }]);705if (!gallery) {706this.logService.info(`MCP server '${name}' not found`);707return true;708}709const local = this.local.find(e => e.name === gallery.name) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), e => this.getRuntimeStatus(e), undefined, gallery, undefined);710this.open(local);711} catch (e) {712// ignore713this.logService.error(e);714}715return true;716}717718async openSearch(searchValue: string, preserveFoucs?: boolean): Promise<void> {719await this.extensionsWorkbenchService.openSearch(`@mcp ${searchValue}`, preserveFoucs);720}721722async open(extension: IWorkbenchMcpServer, options?: IEditorOptions): Promise<void> {723await this.editorService.openEditor(this.instantiationService.createInstance(McpServerEditorInput, extension), options, MODAL_GROUP);724}725726private getInstallState(extension: McpWorkbenchServer): McpServerInstallState {727if (this.installing.some(i => i.name === extension.name)) {728return McpServerInstallState.Installing;729}730if (this.uninstalling.some(e => e.name === extension.name)) {731return McpServerInstallState.Uninstalling;732}733const local = this.local.find(e => e === extension);734return local ? McpServerInstallState.Installed : McpServerInstallState.Uninstalled;735}736737private getRuntimeStatus(mcpServer: McpWorkbenchServer): McpServerEnablementStatus | undefined {738const enablementStatus = this.getEnablementStatus(mcpServer);739740if (enablementStatus) {741return enablementStatus;742}743744if (!this.mcpService.servers.get().find(s => s.definition.id === mcpServer.id)) {745return { state: McpServerEnablementState.Disabled };746}747748return undefined;749}750751private getEnablementStatus(mcpServer: McpWorkbenchServer): McpServerEnablementStatus | undefined {752if (!mcpServer.local) {753return undefined;754}755756const settingsCommandLink = createCommandUri('workbench.action.openSettings', { query: `@id:${mcpAccessConfig}` }).toString();757const accessValue = this.configurationService.getValue(mcpAccessConfig);758759if (accessValue === McpAccessValue.None) {760return {761state: McpServerEnablementState.DisabledByAccess,762message: {763severity: Severity.Warning,764text: 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))765}766};767768}769770if (accessValue === McpAccessValue.Registry) {771if (!mcpServer.gallery) {772return {773state: McpServerEnablementState.DisabledByAccess,774message: {775severity: Severity.Warning,776text: 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))777}778};779}780781const remoteUrl = mcpServer.local.config.type === McpServerType.REMOTE && mcpServer.local.config.url;782if (remoteUrl && !mcpServer.gallery.configuration.remotes?.some(remote => remote.url === remoteUrl)) {783return {784state: McpServerEnablementState.DisabledByAccess,785message: {786severity: Severity.Warning,787text: 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))788}789};790}791}792793return undefined;794}795796}797798export class MCPContextsInitialisation extends Disposable implements IWorkbenchContribution {799800static ID = 'workbench.mcp.contexts.initialisation';801802constructor(803@IMcpWorkbenchService mcpWorkbenchService: IMcpWorkbenchService,804@IMcpGalleryManifestService mcpGalleryManifestService: IMcpGalleryManifestService,805@IContextKeyService contextKeyService: IContextKeyService,806) {807super();808809const mcpServersGalleryStatus = McpServersGalleryStatusContext.bindTo(contextKeyService);810mcpServersGalleryStatus.set(mcpGalleryManifestService.mcpGalleryManifestStatus);811this._register(mcpGalleryManifestService.onDidChangeMcpGalleryManifestStatus(status => mcpServersGalleryStatus.set(status)));812813const hasInstalledMcpServersContextKey = HasInstalledMcpServersContext.bindTo(contextKeyService);814mcpWorkbenchService.queryLocal().finally(() => {815hasInstalledMcpServersContextKey.set(mcpWorkbenchService.local.length > 0);816this._register(mcpWorkbenchService.onChange(() => hasInstalledMcpServersContextKey.set(mcpWorkbenchService.local.length > 0)));817});818}819}820821822