Path: blob/main/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts
3296 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 { 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 } 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 { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js';33import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';34import { 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';35import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';36import { mcpConfigurationSection } from '../common/mcpConfiguration.js';37import { McpServerInstallData, McpServerInstallClassification } from '../common/mcpServer.js';38import { HasInstalledMcpServersContext, IMcpConfigPath, IMcpWorkbenchService, IWorkbenchMcpServer, McpCollectionSortOrder, McpServerEnablementState, McpServerInstallState, McpServersGalleryStatusContext } from '../common/mcpTypes.js';39import { McpServerEditorInput } from './mcpServerEditorInput.js';40import { IMcpGalleryManifestService } from '../../../../platform/mcp/common/mcpGalleryManifest.js';41import { IPager, singlePagePager } from '../../../../base/common/paging.js';4243interface IMcpServerStateProvider<T> {44(mcpWorkbenchServer: McpWorkbenchServer): T;45}4647class McpWorkbenchServer implements IWorkbenchMcpServer {4849constructor(50private installStateProvider: IMcpServerStateProvider<McpServerInstallState>,51public local: IWorkbenchLocalMcpServer | undefined,52public gallery: IGalleryMcpServer | undefined,53public readonly installable: IInstallableMcpServer | undefined,54@IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService,55@IFileService private readonly fileService: IFileService,56@IConfigurationService private readonly configurationService: IConfigurationService,57) {58this.local = local;59}6061get id(): string {62return this.local?.id ?? this.gallery?.id ?? this.installable?.name ?? this.name;63}6465get name(): string {66return this.gallery?.name ?? this.local?.name ?? this.installable?.name ?? '';67}6869get label(): string {70return this.gallery?.displayName ?? this.local?.displayName ?? this.local?.name ?? this.installable?.name ?? '';71}7273get icon(): {74readonly dark: string;75readonly light: string;76} | undefined {77return this.gallery?.icon ?? this.local?.icon;78}7980get installState(): McpServerInstallState {81return this.installStateProvider(this);82}8384get codicon(): string | undefined {85return this.gallery?.codicon ?? this.local?.codicon;86}8788get publisherDisplayName(): string | undefined {89return this.gallery?.publisherDisplayName ?? this.local?.publisherDisplayName ?? this.gallery?.publisher ?? this.local?.publisher;90}9192get publisherUrl(): string | undefined {93return this.gallery?.publisherDomain?.link;94}9596get description(): string {97return this.gallery?.description ?? this.local?.description ?? '';98}99100get starsCount(): number {101return this.gallery?.starsCount ?? 0;102}103104get url(): string | undefined {105return this.gallery?.url;106}107108get repository(): string | undefined {109return this.gallery?.repositoryUrl;110}111112get config(): IMcpServerConfiguration | undefined {113return this.local?.config ?? this.installable?.config;114}115116get enablementState(): McpServerEnablementState {117const accessValue = this.configurationService.getValue(mcpAccessConfig);118if (accessValue === McpAccessValue.None) {119return McpServerEnablementState.DisabledByAccess;120}121if (accessValue === McpAccessValue.Registry && !this.gallery) {122return McpServerEnablementState.DisabledByAccess;123}124return McpServerEnablementState.Enabled;125}126127get readmeUrl(): URI | undefined {128return this.local?.readmeUrl ?? (this.gallery?.readmeUrl ? URI.parse(this.gallery.readmeUrl) : undefined);129}130131async getReadme(token: CancellationToken): Promise<string> {132if (this.local?.readmeUrl) {133const content = await this.fileService.readFile(this.local.readmeUrl);134return content.value.toString();135}136137if (this.gallery?.readme) {138return this.gallery.readme;139}140141if (this.gallery?.readmeUrl) {142return this.mcpGalleryService.getReadme(this.gallery, token);143}144145return Promise.reject(new Error('not available'));146}147148async getManifest(token: CancellationToken): Promise<IGalleryMcpServerConfiguration> {149if (this.local?.manifest) {150return this.local.manifest;151}152153if (this.gallery) {154return this.mcpGalleryService.getMcpServerConfiguration(this.gallery, token);155}156157throw new Error('No manifest available');158}159160}161162export class McpWorkbenchService extends Disposable implements IMcpWorkbenchService {163164_serviceBrand: undefined;165166private installing: McpWorkbenchServer[] = [];167private uninstalling: McpWorkbenchServer[] = [];168169private _local: McpWorkbenchServer[] = [];170get local(): readonly McpWorkbenchServer[] { return [...this._local]; }171172private readonly _onChange = this._register(new Emitter<IWorkbenchMcpServer | undefined>());173readonly onChange = this._onChange.event;174175private readonly _onReset = this._register(new Emitter<void>());176readonly onReset = this._onReset.event;177178constructor(179@IMcpGalleryManifestService mcpGalleryManifestService: IMcpGalleryManifestService,180@IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService,181@IWorkbenchMcpManagementService private readonly mcpManagementService: IWorkbenchMcpManagementService,182@IEditorService private readonly editorService: IEditorService,183@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,184@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,185@IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService,186@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,187@ILabelService private readonly labelService: ILabelService,188@IProductService private readonly productService: IProductService,189@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,190@IConfigurationService private readonly configurationService: IConfigurationService,191@IInstantiationService private readonly instantiationService: IInstantiationService,192@ITelemetryService private readonly telemetryService: ITelemetryService,193@ILogService private readonly logService: ILogService,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(() => this.syncInstalledMcpServers());202urlService.registerHandler(this);203this._register(this.configurationService.onDidChangeConfiguration(e => {204if (e.affectsConfiguration(mcpAccessConfig)) {205this._onChange.fire(undefined);206}207}));208this._register(mcpGalleryManifestService.onDidChangeMcpGalleryManifest(e => this.syncInstalledMcpServers(true)));209}210211private async onDidChangeProfile() {212await this.queryLocal();213this._onChange.fire(undefined);214this._onReset.fire();215}216217private areSameMcpServers(a: { name: string; scope: LocalMcpServerScope } | undefined, b: { name: string; scope: LocalMcpServerScope } | undefined): boolean {218if (a === b) {219return true;220}221if (!a || !b) {222return false;223}224return a.name === b.name && a.scope === b.scope;225}226227private onDidUninstallMcpServer(e: DidUninstallWorkbenchMcpServerEvent) {228if (e.error) {229return;230}231const uninstalled = this._local.find(server => this.areSameMcpServers(server.local, e));232if (uninstalled) {233this._local = this._local.filter(server => server !== uninstalled);234this._onChange.fire(uninstalled);235}236}237238private onDidInstallMcpServers(e: readonly IWorkbenchMcpServerInstallResult[]) {239const servers: IWorkbenchMcpServer[] = [];240for (const { local, source, name } of e) {241let server = this.installing.find(server => server.local && local ? this.areSameMcpServers(server.local, local) : server.name === name);242this.installing = server ? this.installing.filter(e => e !== server) : this.installing;243if (local) {244if (server) {245server.local = local;246} else {247server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), local, source, undefined);248}249if (!local.galleryUrl) {250server.gallery = undefined;251}252this._local = this._local.filter(server => !this.areSameMcpServers(server.local, local));253this._local.push(server);254}255this._onChange.fire(server);256}257if (servers.some(server => server.local?.galleryUrl && !server.gallery)) {258this.syncInstalledMcpServers();259}260}261262private onDidUpdateMcpServers(e: readonly IWorkbenchMcpServerInstallResult[]) {263for (const result of e) {264if (!result.local) {265continue;266}267const serverIndex = this._local.findIndex(server => this.areSameMcpServers(server.local, result.local));268let server: McpWorkbenchServer;269if (serverIndex !== -1) {270this._local[serverIndex].local = result.local;271server = this._local[serverIndex];272} else {273server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), result.local, result.source, undefined);274this._local.push(server);275}276this._onChange.fire(server);277}278}279280private fromGallery(gallery: IGalleryMcpServer): IWorkbenchMcpServer | undefined {281for (const local of this._local) {282if (local.name === gallery.name) {283local.gallery = gallery;284return local;285}286}287return undefined;288}289290private async syncInstalledMcpServers(resetGallery?: boolean): Promise<void> {291const galleryMcpServerUrls: string[] = [];292const vscodeGalleryMcpServerNames: string[] = [];293294for (const installed of this.local) {295if (installed.local?.source !== 'gallery') {296continue;297}298if (installed.local.galleryUrl) {299galleryMcpServerUrls.push(installed.local.galleryUrl);300} else if (!installed.local.manifest) {301vscodeGalleryMcpServerNames.push(installed.local.name);302}303}304305if (galleryMcpServerUrls.length) {306const galleryServers = await this.mcpGalleryService.getMcpServersFromGallery(galleryMcpServerUrls);307if (galleryServers.length) {308await this.syncInstalledMcpServersWithGallery(galleryServers, false, resetGallery);309}310}311312if (vscodeGalleryMcpServerNames.length) {313const galleryServers = await this.mcpGalleryService.getMcpServersFromVSCodeGallery(vscodeGalleryMcpServerNames);314if (galleryServers.length) {315await this.syncInstalledMcpServersWithGallery(galleryServers, true, resetGallery);316}317}318}319320private async syncInstalledMcpServersWithGallery(gallery: IGalleryMcpServer[], vscodeGallery: boolean, resetGallery?: boolean): Promise<void> {321const galleryMap = new Map<string, IGalleryMcpServer>(gallery.map(server => [vscodeGallery ? server.name : (server.url ?? server.name), server]));322for (const mcpServer of this.local) {323if (!mcpServer.local) {324continue;325}326const key = vscodeGallery ? mcpServer.local.name : mcpServer.local.galleryUrl;327const galleryServer = key ? galleryMap.get(key) : undefined;328if (!galleryServer) {329if (mcpServer.gallery && resetGallery) {330mcpServer.gallery = undefined;331this._onChange.fire(mcpServer);332}333continue;334}335if (!vscodeGallery) {336mcpServer.gallery = galleryServer;337}338if (!mcpServer.local.manifest) {339mcpServer.local = await this.mcpManagementService.updateMetadata(mcpServer.local, galleryServer);340}341this._onChange.fire(mcpServer);342}343}344345async queryGallery(options?: IQueryOptions, token?: CancellationToken): Promise<IPager<IWorkbenchMcpServer>> {346if (!this.mcpGalleryService.isEnabled()) {347return singlePagePager([]);348}349const pager = await this.mcpGalleryService.query(options, token);350return {351firstPage: pager.firstPage.map(gallery => this.fromGallery(gallery) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, gallery, undefined)),352total: pager.total,353pageSize: pager.pageSize,354getPage: async (pageIndex, token) => {355const page = await pager.getPage(pageIndex, token);356return page.map(gallery => this.fromGallery(gallery) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, gallery, undefined));357}358};359}360361async queryLocal(): Promise<IWorkbenchMcpServer[]> {362const installed = await this.mcpManagementService.getInstalled();363this._local = installed.map(i => {364const existing = this._local.find(local => {365if (i.galleryUrl) {366return local.local?.galleryUrl === i.galleryUrl;367}368return local.id === i.id;369});370const local = existing ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, undefined, undefined);371local.local = i;372return local;373});374this._onChange.fire(undefined);375return [...this.local];376}377378getEnabledLocalMcpServers(): IWorkbenchLocalMcpServer[] {379const result = new Map<string, IWorkbenchLocalMcpServer>();380const userRemote: IWorkbenchLocalMcpServer[] = [];381const workspace: IWorkbenchLocalMcpServer[] = [];382383for (const server of this.local) {384if (server.enablementState !== McpServerEnablementState.Enabled) {385continue;386}387388if (server.local?.scope === LocalMcpServerScope.User) {389result.set(server.name, server.local);390} else if (server.local?.scope === LocalMcpServerScope.RemoteUser) {391userRemote.push(server.local);392} else if (server.local?.scope === LocalMcpServerScope.Workspace) {393workspace.push(server.local);394}395}396397for (const server of userRemote) {398const existing = result.get(server.name);399if (existing) {400this.logService.warn(localize('overwriting', "Overwriting mcp server '{0}' from {1} with {2}.", server.name, server.mcpResource.path, existing.mcpResource.path));401}402result.set(server.name, server);403}404405for (const server of workspace) {406const existing = result.get(server.name);407if (existing) {408this.logService.warn(localize('overwriting', "Overwriting mcp server '{0}' from {1} with {2}.", server.name, server.mcpResource.path, existing.mcpResource.path));409}410result.set(server.name, server);411}412413return [...result.values()];414}415416canInstall(mcpServer: IWorkbenchMcpServer): true | IMarkdownString {417if (!(mcpServer instanceof McpWorkbenchServer)) {418return new MarkdownString().appendText(localize('not an extension', "The provided object is not an mcp server."));419}420421if (mcpServer.gallery) {422const result = this.mcpManagementService.canInstall(mcpServer.gallery);423if (result === true) {424return true;425}426427return result;428}429430if (mcpServer.installable) {431const result = this.mcpManagementService.canInstall(mcpServer.installable);432if (result === true) {433return true;434}435436return result;437}438439440return new MarkdownString().appendText(localize('cannot be installed', "Cannot install the '{0}' MCP Server because it is not available in this setup.", mcpServer.label));441}442443async install(server: IWorkbenchMcpServer): Promise<IWorkbenchMcpServer> {444if (!(server instanceof McpWorkbenchServer)) {445throw new Error('Invalid server instance');446}447448if (server.installable) {449const installable = server.installable;450return this.doInstall(server, () => this.mcpManagementService.install(installable));451}452453if (server.gallery) {454const gallery = server.gallery;455return this.doInstall(server, () => this.mcpManagementService.installFromGallery(gallery));456}457458throw new Error('No installable server found');459}460461async uninstall(server: IWorkbenchMcpServer): Promise<void> {462if (!server.local) {463throw new Error('Local server is missing');464}465await this.mcpManagementService.uninstall(server.local);466}467468private async doInstall(server: McpWorkbenchServer, installTask: () => Promise<IWorkbenchLocalMcpServer>): Promise<IWorkbenchMcpServer> {469const source = server.gallery ? 'gallery' : 'local';470const serverName = server.name;471// Check for inputs in installable config or if it comes from handleURL with inputs472const hasInputs = !!(server.installable?.inputs && server.installable.inputs.length > 0);473474this.installing.push(server);475this._onChange.fire(server);476477try {478await installTask();479const result = await this.waitAndGetInstalledMcpServer(server);480481// Track successful installation482this.telemetryService.publicLog2<McpServerInstallData, McpServerInstallClassification>('mcp/serverInstall', {483serverName,484source,485scope: result.local?.scope ?? 'unknown',486success: true,487hasInputs488});489490return result;491} catch (error) {492// Track failed installation493this.telemetryService.publicLog2<McpServerInstallData, McpServerInstallClassification>('mcp/serverInstall', {494serverName,495source,496scope: 'unknown',497success: false,498error: error instanceof Error ? error.message : String(error),499hasInputs500});501502throw error;503}504}505506private async waitAndGetInstalledMcpServer(server: McpWorkbenchServer): Promise<IWorkbenchMcpServer> {507let installed = this.local.find(local => local.name === server.name);508if (!installed) {509await Event.toPromise(Event.filter(this.onChange, e => !!e && this.local.some(local => local.name === server.name)));510}511installed = this.local.find(local => local.name === server.name);512if (!installed) {513// This should not happen514throw new Error('Extension should have been installed');515}516return installed;517}518519getMcpConfigPath(localMcpServer: IWorkbenchLocalMcpServer): IMcpConfigPath | undefined;520getMcpConfigPath(mcpResource: URI): Promise<IMcpConfigPath | undefined>;521getMcpConfigPath(arg: URI | IWorkbenchLocalMcpServer): Promise<IMcpConfigPath | undefined> | IMcpConfigPath | undefined {522if (arg instanceof URI) {523const mcpResource = arg;524for (const profile of this.userDataProfilesService.profiles) {525if (this.uriIdentityService.extUri.isEqual(profile.mcpResource, mcpResource)) {526return this.getUserMcpConfigPath(mcpResource);527}528}529530return this.remoteAgentService.getEnvironment().then(remoteEnvironment => {531if (remoteEnvironment && this.uriIdentityService.extUri.isEqual(remoteEnvironment.mcpResource, mcpResource)) {532return this.getRemoteMcpConfigPath(mcpResource);533}534return this.getWorkspaceMcpConfigPath(mcpResource);535});536}537538if (arg.scope === LocalMcpServerScope.User) {539return this.getUserMcpConfigPath(arg.mcpResource);540}541542if (arg.scope === LocalMcpServerScope.Workspace) {543return this.getWorkspaceMcpConfigPath(arg.mcpResource);544}545546if (arg.scope === LocalMcpServerScope.RemoteUser) {547return this.getRemoteMcpConfigPath(arg.mcpResource);548}549550return undefined;551}552553private getUserMcpConfigPath(mcpResource: URI): IMcpConfigPath {554return {555id: USER_CONFIG_ID,556key: 'userLocalValue',557target: ConfigurationTarget.USER_LOCAL,558label: localize('mcp.configuration.userLocalValue', 'Global in {0}', this.productService.nameShort),559scope: StorageScope.PROFILE,560order: McpCollectionSortOrder.User,561uri: mcpResource,562section: [],563};564}565566private getRemoteMcpConfigPath(mcpResource: URI): IMcpConfigPath {567return {568id: REMOTE_USER_CONFIG_ID,569key: 'userRemoteValue',570target: ConfigurationTarget.USER_REMOTE,571label: this.environmentService.remoteAuthority ? this.labelService.getHostLabel(Schemas.vscodeRemote, this.environmentService.remoteAuthority) : 'Remote',572scope: StorageScope.PROFILE,573order: McpCollectionSortOrder.User + McpCollectionSortOrder.RemoteBoost,574remoteAuthority: this.environmentService.remoteAuthority,575uri: mcpResource,576section: [],577};578}579580private getWorkspaceMcpConfigPath(mcpResource: URI): IMcpConfigPath | undefined {581const workspace = this.workspaceService.getWorkspace();582if (workspace.configuration && this.uriIdentityService.extUri.isEqual(workspace.configuration, mcpResource)) {583return {584id: WORKSPACE_CONFIG_ID,585key: 'workspaceValue',586target: ConfigurationTarget.WORKSPACE,587label: basename(mcpResource),588scope: StorageScope.WORKSPACE,589order: McpCollectionSortOrder.Workspace,590remoteAuthority: this.environmentService.remoteAuthority,591uri: mcpResource,592section: ['settings', mcpConfigurationSection],593};594}595596const workspaceFolders = workspace.folders;597for (let index = 0; index < workspaceFolders.length; index++) {598const workspaceFolder = workspaceFolders[index];599if (this.uriIdentityService.extUri.isEqual(this.uriIdentityService.extUri.joinPath(workspaceFolder.uri, WORKSPACE_STANDALONE_CONFIGURATIONS[MCP_CONFIGURATION_KEY]), mcpResource)) {600return {601id: `${WORKSPACE_FOLDER_CONFIG_ID_PREFIX}${index}`,602key: 'workspaceFolderValue',603target: ConfigurationTarget.WORKSPACE_FOLDER,604label: `${workspaceFolder.name}/.vscode/mcp.json`,605scope: StorageScope.WORKSPACE,606remoteAuthority: this.environmentService.remoteAuthority,607order: McpCollectionSortOrder.WorkspaceFolder,608uri: mcpResource,609workspaceFolder,610};611}612}613614return undefined;615}616617async handleURL(uri: URI): Promise<boolean> {618if (uri.path === 'mcp/install') {619return this.handleMcpInstallUri(uri);620}621if (uri.path.startsWith('mcp/')) {622const mcpServerUrl = uri.path.substring(4);623if (mcpServerUrl) {624return this.handleMcpServerUrl(`${Schemas.https}://${mcpServerUrl}`);625}626}627return false;628}629630private async handleMcpInstallUri(uri: URI): Promise<boolean> {631let parsed: IMcpServerConfiguration & { name: string; inputs?: IMcpServerVariable[]; gallery?: boolean };632try {633parsed = JSON.parse(decodeURIComponent(uri.query));634} catch (e) {635return false;636}637638try {639const { name, inputs, gallery, ...config } = parsed;640641if (gallery || !config || Object.keys(config).length === 0) {642const [galleryServer] = await this.mcpGalleryService.getMcpServersFromVSCodeGallery([name]);643if (!galleryServer) {644throw new Error(`MCP server '${name}' not found in gallery`);645}646const local = this.local.find(e => e.name === name && e.local?.scope !== LocalMcpServerScope.Workspace)647?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, galleryServer, undefined);648this.open(local);649} else {650if (config.type === undefined) {651(<Mutable<IMcpServerConfiguration>>config).type = (<IMcpStdioServerConfiguration>parsed).command ? McpServerType.LOCAL : McpServerType.REMOTE;652}653this.open(this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, undefined, { name, config, inputs }));654}655} catch (e) {656// ignore657}658return true;659}660661private async handleMcpServerUrl(url: string): Promise<boolean> {662try {663const gallery = await this.mcpGalleryService.getMcpServer(url);664if (!gallery) {665this.logService.info(`MCP server '${url}' not found`);666return true;667}668const local = this.local.find(e => e.url === url) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, gallery, undefined);669this.open(local);670} catch (e) {671// ignore672this.logService.error(e);673}674return true;675}676677async open(extension: IWorkbenchMcpServer, options?: IEditorOptions): Promise<void> {678await this.editorService.openEditor(this.instantiationService.createInstance(McpServerEditorInput, extension), options, ACTIVE_GROUP);679}680681private getInstallState(extension: McpWorkbenchServer): McpServerInstallState {682if (this.installing.some(i => i.name === extension.name)) {683return McpServerInstallState.Installing;684}685if (this.uninstalling.some(e => e.name === extension.name)) {686return McpServerInstallState.Uninstalling;687}688const local = this.local.find(e => e === extension);689return local ? McpServerInstallState.Installed : McpServerInstallState.Uninstalled;690}691692}693694export class MCPContextsInitialisation extends Disposable implements IWorkbenchContribution {695696static ID = 'workbench.mcp.contexts.initialisation';697698constructor(699@IMcpWorkbenchService mcpWorkbenchService: IMcpWorkbenchService,700@IMcpGalleryManifestService mcpGalleryManifestService: IMcpGalleryManifestService,701@IContextKeyService contextKeyService: IContextKeyService,702) {703super();704705const mcpServersGalleryStatus = McpServersGalleryStatusContext.bindTo(contextKeyService);706mcpServersGalleryStatus.set(mcpGalleryManifestService.mcpGalleryManifestStatus);707this._register(mcpGalleryManifestService.onDidChangeMcpGalleryManifestStatus(status => mcpServersGalleryStatus.set(status)));708709const hasInstalledMcpServersContextKey = HasInstalledMcpServersContext.bindTo(contextKeyService);710hasInstalledMcpServersContextKey.set(mcpWorkbenchService.local.length > 0);711this._register(mcpWorkbenchService.onChange(() => hasInstalledMcpServersContextKey.set(mcpWorkbenchService.local.length > 0)));712}713}714715716