Path: blob/main/src/vs/platform/mcp/common/mcpManagementService.ts
5252 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 { RunOnceScheduler } from '../../../base/common/async.js';6import { VSBuffer } from '../../../base/common/buffer.js';7import { CancellationToken } from '../../../base/common/cancellation.js';8import { Emitter, Event } from '../../../base/common/event.js';9import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js';10import { Disposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';11import { ResourceMap } from '../../../base/common/map.js';12import { equals } from '../../../base/common/objects.js';13import { isString } from '../../../base/common/types.js';14import { URI } from '../../../base/common/uri.js';15import { localize } from '../../../nls.js';16import { ConfigurationTarget } from '../../configuration/common/configuration.js';17import { IEnvironmentService } from '../../environment/common/environment.js';18import { IFileService } from '../../files/common/files.js';19import { IInstantiationService } from '../../instantiation/common/instantiation.js';20import { ILogService } from '../../log/common/log.js';21import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';22import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';23import { DidUninstallMcpServerEvent, IGalleryMcpServer, ILocalMcpServer, IMcpGalleryService, IMcpManagementService, IMcpServerInput, IGalleryMcpServerConfiguration, InstallMcpServerEvent, InstallMcpServerResult, RegistryType, UninstallMcpServerEvent, InstallOptions, UninstallOptions, IInstallableMcpServer, IAllowedMcpServersService, IMcpServerArgument, IMcpServerKeyValueInput, McpServerConfigurationParseResult } from './mcpManagement.js';24import { IMcpServerVariable, McpServerVariableType, IMcpServerConfiguration, McpServerType } from './mcpPlatformTypes.js';25import { IMcpResourceScannerService, McpResourceTarget } from './mcpResourceScannerService.js';2627export interface ILocalMcpServerInfo {28name: string;29version?: string;30displayName?: string;31galleryId?: string;32galleryUrl?: string;33description?: string;34repositoryUrl?: string;35publisher?: string;36publisherDisplayName?: string;37icon?: {38dark: string;39light: string;40};41codicon?: string;42manifest?: IGalleryMcpServerConfiguration;43readmeUrl?: URI;44location?: URI;45licenseUrl?: string;46}4748export abstract class AbstractCommonMcpManagementService extends Disposable implements IMcpManagementService {4950_serviceBrand: undefined;5152abstract onInstallMcpServer: Event<InstallMcpServerEvent>;53abstract onDidInstallMcpServers: Event<readonly InstallMcpServerResult[]>;54abstract onDidUpdateMcpServers: Event<readonly InstallMcpServerResult[]>;55abstract onUninstallMcpServer: Event<UninstallMcpServerEvent>;56abstract onDidUninstallMcpServer: Event<DidUninstallMcpServerEvent>;5758abstract getInstalled(mcpResource?: URI): Promise<ILocalMcpServer[]>;59abstract install(server: IInstallableMcpServer, options?: InstallOptions): Promise<ILocalMcpServer>;60abstract installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer>;61abstract updateMetadata(local: ILocalMcpServer, server: IGalleryMcpServer, profileLocation?: URI): Promise<ILocalMcpServer>;62abstract uninstall(server: ILocalMcpServer, options?: UninstallOptions): Promise<void>;63abstract canInstall(server: IGalleryMcpServer | IInstallableMcpServer): true | IMarkdownString;6465constructor(66@ILogService protected readonly logService: ILogService67) {68super();69}7071getMcpServerConfigurationFromManifest(manifest: IGalleryMcpServerConfiguration, packageType: RegistryType): McpServerConfigurationParseResult {7273// remote74if (packageType === RegistryType.REMOTE && manifest.remotes?.length) {75const url = manifest.remotes[0].url;76const headers = manifest.remotes[0].headers ?? [];77const { inputs, variables } = this.processKeyValueInputs(url.startsWith('https://api.githubcopilot.com/mcp') ? headers.filter(h => h.name.toLowerCase() !== 'authorization') : headers);78return {79mcpServerConfiguration: {80config: {81type: McpServerType.REMOTE,82url: manifest.remotes[0].url,83headers: Object.keys(inputs).length ? inputs : undefined,84},85inputs: variables.length ? variables : undefined,86},87notices: [],88};89}9091// local92const serverPackage = manifest.packages?.find(p => p.registryType === packageType) ?? manifest.packages?.[0];93if (!serverPackage) {94throw new Error(`No server package found`);95}9697const args: string[] = [];98const inputs: IMcpServerVariable[] = [];99const env: Record<string, string> = {};100const notices: string[] = [];101102if (serverPackage.registryType === RegistryType.DOCKER) {103args.push('run');104args.push('-i');105args.push('--rm');106}107108if (serverPackage.runtimeArguments?.length) {109const result = this.processArguments(serverPackage.runtimeArguments ?? []);110args.push(...result.args);111inputs.push(...result.variables);112notices.push(...result.notices);113}114115if (serverPackage.environmentVariables?.length) {116const { inputs: envInputs, variables: envVariables, notices: envNotices } = this.processKeyValueInputs(serverPackage.environmentVariables ?? []);117inputs.push(...envVariables);118notices.push(...envNotices);119for (const [name, value] of Object.entries(envInputs)) {120env[name] = value;121if (serverPackage.registryType === RegistryType.DOCKER) {122args.push('-e');123args.push(name);124}125}126}127128switch (serverPackage.registryType) {129case RegistryType.NODE:130if (serverPackage.registryBaseUrl) {131args.push('--registry', serverPackage.registryBaseUrl);132}133args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier);134break;135case RegistryType.PYTHON:136if (serverPackage.registryBaseUrl) {137args.push('--index-url', serverPackage.registryBaseUrl);138}139args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier);140break;141case RegistryType.DOCKER:142{143const dockerIdentifier = serverPackage.registryBaseUrl144? `${serverPackage.registryBaseUrl}/${serverPackage.identifier}`145: serverPackage.identifier;146args.push(serverPackage.version ? `${dockerIdentifier}:${serverPackage.version}` : dockerIdentifier);147break;148}149case RegistryType.NUGET:150args.push(serverPackage.version ? `${serverPackage.identifier}@${serverPackage.version}` : serverPackage.identifier);151args.push('--yes'); // installation is confirmed by the UI, so --yes is appropriate here152if (serverPackage.registryBaseUrl) {153args.push('--source', serverPackage.registryBaseUrl);154}155if (serverPackage.packageArguments?.length) {156args.push('--');157}158break;159}160161if (serverPackage.packageArguments?.length) {162const result = this.processArguments(serverPackage.packageArguments);163args.push(...result.args);164inputs.push(...result.variables);165notices.push(...result.notices);166}167168return {169notices,170mcpServerConfiguration: {171config: {172type: McpServerType.LOCAL,173command: this.getCommandName(serverPackage.registryType),174args: args.length ? args : undefined,175env: Object.keys(env).length ? env : undefined,176},177inputs: inputs.length ? inputs : undefined,178}179};180}181182protected getCommandName(packageType: RegistryType): string {183switch (packageType) {184case RegistryType.NODE: return 'npx';185case RegistryType.DOCKER: return 'docker';186case RegistryType.PYTHON: return 'uvx';187case RegistryType.NUGET: return 'dnx';188}189return packageType;190}191192protected getVariables(variableInputs: Record<string, IMcpServerInput>): IMcpServerVariable[] {193const variables: IMcpServerVariable[] = [];194for (const [key, value] of Object.entries(variableInputs)) {195variables.push({196id: key,197type: value.choices ? McpServerVariableType.PICK : McpServerVariableType.PROMPT,198description: value.description ?? '',199password: !!value.isSecret,200default: value.default,201options: value.choices,202});203}204return variables;205}206207private processKeyValueInputs(keyValueInputs: ReadonlyArray<IMcpServerKeyValueInput>): { inputs: Record<string, string>; variables: IMcpServerVariable[]; notices: string[] } {208const notices: string[] = [];209const inputs: Record<string, string> = {};210const variables: IMcpServerVariable[] = [];211212for (const input of keyValueInputs) {213const inputVariables = input.variables ? this.getVariables(input.variables) : [];214let value = input.value || '';215216// If explicit variables exist, use them regardless of value217if (inputVariables.length) {218for (const variable of inputVariables) {219value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`);220}221variables.push(...inputVariables);222} else if (!value && (input.description || input.choices || input.default !== undefined)) {223// Only create auto-generated input variable if no explicit variables and no value224variables.push({225id: input.name,226type: input.choices ? McpServerVariableType.PICK : McpServerVariableType.PROMPT,227description: input.description ?? '',228password: !!input.isSecret,229default: input.default,230options: input.choices,231});232value = `\${input:${input.name}}`;233}234235inputs[input.name] = value;236}237238return { inputs, variables, notices };239}240241private processArguments(argumentsList: readonly IMcpServerArgument[]): { args: string[]; variables: IMcpServerVariable[]; notices: string[] } {242const args: string[] = [];243const variables: IMcpServerVariable[] = [];244const notices: string[] = [];245for (const arg of argumentsList) {246const argVariables = arg.variables ? this.getVariables(arg.variables) : [];247248if (arg.type === 'positional') {249let value = arg.value;250if (value) {251for (const variable of argVariables) {252value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`);253}254args.push(value);255if (argVariables.length) {256variables.push(...argVariables);257}258} else if (arg.valueHint && (arg.description || arg.default !== undefined)) {259// Create input variable for positional argument without value260variables.push({261id: arg.valueHint,262type: McpServerVariableType.PROMPT,263description: arg.description ?? '',264password: false,265default: arg.default,266});267args.push(`\${input:${arg.valueHint}}`);268} else {269// Fallback to value_hint as literal270args.push(arg.valueHint ?? '');271}272} else if (arg.type === 'named') {273if (!arg.name) {274notices.push(`Named argument is missing a name. ${JSON.stringify(arg)}`);275continue;276}277args.push(arg.name);278if (arg.value) {279let value = arg.value;280for (const variable of argVariables) {281value = value.replace(`{${variable.id}}`, `\${input:${variable.id}}`);282}283args.push(value);284if (argVariables.length) {285variables.push(...argVariables);286}287} else if (arg.description || arg.default !== undefined) {288// Create input variable for named argument without value289const variableId = arg.name.replace(/^--?/, '');290variables.push({291id: variableId,292type: McpServerVariableType.PROMPT,293description: arg.description ?? '',294password: false,295default: arg.default,296});297args.push(`\${input:${variableId}}`);298}299}300}301return { args, variables, notices };302}303304}305306export abstract class AbstractMcpResourceManagementService extends AbstractCommonMcpManagementService {307308private initializePromise: Promise<void> | undefined;309private readonly reloadConfigurationScheduler: RunOnceScheduler;310private local = new Map<string, ILocalMcpServer>();311312protected readonly _onInstallMcpServer = this._register(new Emitter<InstallMcpServerEvent>());313readonly onInstallMcpServer = this._onInstallMcpServer.event;314315protected readonly _onDidInstallMcpServers = this._register(new Emitter<InstallMcpServerResult[]>());316get onDidInstallMcpServers() { return this._onDidInstallMcpServers.event; }317318protected readonly _onDidUpdateMcpServers = this._register(new Emitter<InstallMcpServerResult[]>());319get onDidUpdateMcpServers() { return this._onDidUpdateMcpServers.event; }320321protected readonly _onUninstallMcpServer = this._register(new Emitter<UninstallMcpServerEvent>());322get onUninstallMcpServer() { return this._onUninstallMcpServer.event; }323324protected _onDidUninstallMcpServer = this._register(new Emitter<DidUninstallMcpServerEvent>());325get onDidUninstallMcpServer() { return this._onDidUninstallMcpServer.event; }326327constructor(328protected readonly mcpResource: URI,329protected readonly target: McpResourceTarget,330@IMcpGalleryService protected readonly mcpGalleryService: IMcpGalleryService,331@IFileService protected readonly fileService: IFileService,332@IUriIdentityService protected readonly uriIdentityService: IUriIdentityService,333@ILogService logService: ILogService,334@IMcpResourceScannerService protected readonly mcpResourceScannerService: IMcpResourceScannerService,335) {336super(logService);337this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.updateLocal(), 50));338}339340private initialize(): Promise<void> {341if (!this.initializePromise) {342this.initializePromise = (async () => {343try {344this.local = await this.populateLocalServers();345} finally {346this.startWatching();347}348})();349}350return this.initializePromise;351}352353private async populateLocalServers(): Promise<Map<string, ILocalMcpServer>> {354this.logService.trace('AbstractMcpResourceManagementService#populateLocalServers', this.mcpResource.toString());355const local = new Map<string, ILocalMcpServer>();356try {357const scannedMcpServers = await this.mcpResourceScannerService.scanMcpServers(this.mcpResource, this.target);358if (scannedMcpServers.servers) {359await Promise.allSettled(Object.entries(scannedMcpServers.servers).map(async ([name, scannedServer]) => {360const server = await this.scanLocalServer(name, scannedServer);361local.set(name, server);362}));363}364} catch (error) {365this.logService.debug('Could not read user MCP servers:', error);366throw error;367}368return local;369}370371private startWatching(): void {372this._register(this.fileService.watch(this.mcpResource));373this._register(this.fileService.onDidFilesChange(e => {374if (e.affects(this.mcpResource)) {375this.reloadConfigurationScheduler.schedule();376}377}));378}379380protected async updateLocal(): Promise<void> {381try {382const current = await this.populateLocalServers();383384const added: ILocalMcpServer[] = [];385const updated: ILocalMcpServer[] = [];386const removed = [...this.local.keys()].filter(name => !current.has(name));387388for (const server of removed) {389this.local.delete(server);390}391392for (const [name, server] of current) {393const previous = this.local.get(name);394if (previous) {395if (!equals(previous, server)) {396updated.push(server);397this.local.set(name, server);398}399} else {400added.push(server);401this.local.set(name, server);402}403}404405for (const server of removed) {406this.local.delete(server);407this._onDidUninstallMcpServer.fire({ name: server, mcpResource: this.mcpResource });408}409410if (updated.length) {411this._onDidUpdateMcpServers.fire(updated.map(server => ({ name: server.name, local: server, mcpResource: this.mcpResource })));412}413414if (added.length) {415this._onDidInstallMcpServers.fire(added.map(server => ({ name: server.name, local: server, mcpResource: this.mcpResource })));416}417418} catch (error) {419this.logService.error('Failed to load installed MCP servers:', error);420}421}422423async getInstalled(): Promise<ILocalMcpServer[]> {424await this.initialize();425return Array.from(this.local.values());426}427428protected async scanLocalServer(name: string, config: IMcpServerConfiguration): Promise<ILocalMcpServer> {429let mcpServerInfo = await this.getLocalServerInfo(name, config);430if (!mcpServerInfo) {431mcpServerInfo = { name, version: config.version, galleryUrl: isString(config.gallery) ? config.gallery : undefined };432}433434return {435name,436config,437mcpResource: this.mcpResource,438version: mcpServerInfo.version,439location: mcpServerInfo.location,440displayName: mcpServerInfo.displayName,441description: mcpServerInfo.description,442publisher: mcpServerInfo.publisher,443publisherDisplayName: mcpServerInfo.publisherDisplayName,444galleryUrl: mcpServerInfo.galleryUrl,445galleryId: mcpServerInfo.galleryId,446repositoryUrl: mcpServerInfo.repositoryUrl,447readmeUrl: mcpServerInfo.readmeUrl,448icon: mcpServerInfo.icon,449codicon: mcpServerInfo.codicon,450manifest: mcpServerInfo.manifest,451source: config.gallery ? 'gallery' : 'local'452};453}454455async install(server: IInstallableMcpServer, options?: Omit<InstallOptions, 'mcpResource'>): Promise<ILocalMcpServer> {456this.logService.trace('MCP Management Service: install', server.name);457458this._onInstallMcpServer.fire({ name: server.name, mcpResource: this.mcpResource });459try {460await this.mcpResourceScannerService.addMcpServers([server], this.mcpResource, this.target);461await this.updateLocal();462const local = this.local.get(server.name);463if (!local) {464throw new Error(`Failed to install MCP server: ${server.name}`);465}466return local;467} catch (e) {468this._onDidInstallMcpServers.fire([{ name: server.name, error: e, mcpResource: this.mcpResource }]);469throw e;470}471}472473async uninstall(server: ILocalMcpServer, options?: Omit<UninstallOptions, 'mcpResource'>): Promise<void> {474this.logService.trace('MCP Management Service: uninstall', server.name);475this._onUninstallMcpServer.fire({ name: server.name, mcpResource: this.mcpResource });476477try {478const currentServers = await this.mcpResourceScannerService.scanMcpServers(this.mcpResource, this.target);479if (!currentServers.servers) {480return;481}482await this.mcpResourceScannerService.removeMcpServers([server.name], this.mcpResource, this.target);483if (server.location) {484await this.fileService.del(URI.revive(server.location), { recursive: true });485}486await this.updateLocal();487} catch (e) {488this._onDidUninstallMcpServer.fire({ name: server.name, error: e, mcpResource: this.mcpResource });489throw e;490}491}492493protected abstract getLocalServerInfo(name: string, mcpServerConfig: IMcpServerConfiguration): Promise<ILocalMcpServerInfo | undefined>;494protected abstract installFromUri(uri: URI, options?: Omit<InstallOptions, 'mcpResource'>): Promise<ILocalMcpServer>;495}496497export class McpUserResourceManagementService extends AbstractMcpResourceManagementService {498499protected readonly mcpLocation: URI;500501constructor(502mcpResource: URI,503@IMcpGalleryService mcpGalleryService: IMcpGalleryService,504@IFileService fileService: IFileService,505@IUriIdentityService uriIdentityService: IUriIdentityService,506@ILogService logService: ILogService,507@IMcpResourceScannerService mcpResourceScannerService: IMcpResourceScannerService,508@IEnvironmentService environmentService: IEnvironmentService509) {510super(mcpResource, ConfigurationTarget.USER, mcpGalleryService, fileService, uriIdentityService, logService, mcpResourceScannerService);511this.mcpLocation = uriIdentityService.extUri.joinPath(environmentService.userRoamingDataHome, 'mcp');512}513514async installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {515throw new Error('Not supported');516}517518async updateMetadata(local: ILocalMcpServer, gallery: IGalleryMcpServer): Promise<ILocalMcpServer> {519await this.updateMetadataFromGallery(gallery);520await this.updateLocal();521const updatedLocal = (await this.getInstalled()).find(s => s.name === local.name);522if (!updatedLocal) {523throw new Error(`Failed to find MCP server: ${local.name}`);524}525return updatedLocal;526}527528protected async updateMetadataFromGallery(gallery: IGalleryMcpServer): Promise<IGalleryMcpServerConfiguration> {529const manifest = gallery.configuration;530const location = this.getLocation(gallery.name, gallery.version);531const manifestPath = this.uriIdentityService.extUri.joinPath(location, 'manifest.json');532const local: ILocalMcpServerInfo = {533galleryUrl: gallery.galleryUrl,534galleryId: gallery.id,535name: gallery.name,536displayName: gallery.displayName,537description: gallery.description,538version: gallery.version,539publisher: gallery.publisher,540publisherDisplayName: gallery.publisherDisplayName,541repositoryUrl: gallery.repositoryUrl,542licenseUrl: gallery.license,543icon: gallery.icon,544codicon: gallery.codicon,545manifest,546};547await this.fileService.writeFile(manifestPath, VSBuffer.fromString(JSON.stringify(local)));548549if (gallery.readmeUrl || gallery.readme) {550const readme = gallery.readme ? gallery.readme : await this.mcpGalleryService.getReadme(gallery, CancellationToken.None);551await this.fileService.writeFile(this.uriIdentityService.extUri.joinPath(location, 'README.md'), VSBuffer.fromString(readme));552}553554return manifest;555}556557protected async getLocalServerInfo(name: string, mcpServerConfig: IMcpServerConfiguration): Promise<ILocalMcpServerInfo | undefined> {558let storedMcpServerInfo: ILocalMcpServerInfo | undefined;559let location: URI | undefined;560let readmeUrl: URI | undefined;561if (mcpServerConfig.gallery) {562location = this.getLocation(name, mcpServerConfig.version);563const manifestLocation = this.uriIdentityService.extUri.joinPath(location, 'manifest.json');564try {565const content = await this.fileService.readFile(manifestLocation);566storedMcpServerInfo = JSON.parse(content.value.toString()) as ILocalMcpServerInfo;567568// migrate569if (storedMcpServerInfo.galleryUrl?.includes('/v0/')) {570storedMcpServerInfo.galleryUrl = storedMcpServerInfo.galleryUrl.substring(0, storedMcpServerInfo.galleryUrl.indexOf('/v0/'));571await this.fileService.writeFile(manifestLocation, VSBuffer.fromString(JSON.stringify(storedMcpServerInfo)));572}573574storedMcpServerInfo.location = location;575readmeUrl = this.uriIdentityService.extUri.joinPath(location, 'README.md');576if (!await this.fileService.exists(readmeUrl)) {577readmeUrl = undefined;578}579storedMcpServerInfo.readmeUrl = readmeUrl;580} catch (e) {581this.logService.error('MCP Management Service: failed to read manifest', location.toString(), e);582}583}584return storedMcpServerInfo;585}586587protected getLocation(name: string, version?: string): URI {588name = name.replace('/', '.');589return this.uriIdentityService.extUri.joinPath(this.mcpLocation, version ? `${name}-${version}` : name);590}591592protected override installFromUri(uri: URI, options?: Omit<InstallOptions, 'mcpResource'>): Promise<ILocalMcpServer> {593throw new Error('Method not supported.');594}595596override canInstall(): true | IMarkdownString {597throw new Error('Not supported');598}599600}601602export abstract class AbstractMcpManagementService extends AbstractCommonMcpManagementService implements IMcpManagementService {603604constructor(605@IAllowedMcpServersService protected readonly allowedMcpServersService: IAllowedMcpServersService,606@ILogService logService: ILogService,607) {608super(logService);609}610611canInstall(server: IGalleryMcpServer | IInstallableMcpServer): true | IMarkdownString {612const allowedToInstall = this.allowedMcpServersService.isAllowed(server);613if (allowedToInstall !== true) {614return new MarkdownString(localize('not allowed to install', "This mcp server cannot be installed because {0}", allowedToInstall.value));615}616return true;617}618}619620export class McpManagementService extends AbstractMcpManagementService implements IMcpManagementService {621622private readonly _onInstallMcpServer = this._register(new Emitter<InstallMcpServerEvent>());623readonly onInstallMcpServer = this._onInstallMcpServer.event;624625private readonly _onDidInstallMcpServers = this._register(new Emitter<readonly InstallMcpServerResult[]>());626readonly onDidInstallMcpServers = this._onDidInstallMcpServers.event;627628private readonly _onDidUpdateMcpServers = this._register(new Emitter<readonly InstallMcpServerResult[]>());629readonly onDidUpdateMcpServers = this._onDidUpdateMcpServers.event;630631private readonly _onUninstallMcpServer = this._register(new Emitter<UninstallMcpServerEvent>());632readonly onUninstallMcpServer = this._onUninstallMcpServer.event;633634private readonly _onDidUninstallMcpServer = this._register(new Emitter<DidUninstallMcpServerEvent>());635readonly onDidUninstallMcpServer = this._onDidUninstallMcpServer.event;636637private readonly mcpResourceManagementServices = new ResourceMap<{ service: McpUserResourceManagementService } & IDisposable>();638639constructor(640@IAllowedMcpServersService allowedMcpServersService: IAllowedMcpServersService,641@ILogService logService: ILogService,642@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,643@IInstantiationService protected readonly instantiationService: IInstantiationService,644) {645super(allowedMcpServersService, logService);646}647648private getMcpResourceManagementService(mcpResource: URI): McpUserResourceManagementService {649let mcpResourceManagementService = this.mcpResourceManagementServices.get(mcpResource);650if (!mcpResourceManagementService) {651const disposables = new DisposableStore();652const service = disposables.add(this.createMcpResourceManagementService(mcpResource));653disposables.add(service.onInstallMcpServer(e => this._onInstallMcpServer.fire(e)));654disposables.add(service.onDidInstallMcpServers(e => this._onDidInstallMcpServers.fire(e)));655disposables.add(service.onDidUpdateMcpServers(e => this._onDidUpdateMcpServers.fire(e)));656disposables.add(service.onUninstallMcpServer(e => this._onUninstallMcpServer.fire(e)));657disposables.add(service.onDidUninstallMcpServer(e => this._onDidUninstallMcpServer.fire(e)));658this.mcpResourceManagementServices.set(mcpResource, mcpResourceManagementService = { service, dispose: () => disposables.dispose() });659}660return mcpResourceManagementService.service;661}662663async getInstalled(mcpResource?: URI): Promise<ILocalMcpServer[]> {664const mcpResourceUri = mcpResource || this.userDataProfilesService.defaultProfile.mcpResource;665return this.getMcpResourceManagementService(mcpResourceUri).getInstalled();666}667668async install(server: IInstallableMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {669const mcpResourceUri = options?.mcpResource || this.userDataProfilesService.defaultProfile.mcpResource;670return this.getMcpResourceManagementService(mcpResourceUri).install(server, options);671}672673async uninstall(server: ILocalMcpServer, options?: UninstallOptions): Promise<void> {674const mcpResourceUri = options?.mcpResource || this.userDataProfilesService.defaultProfile.mcpResource;675return this.getMcpResourceManagementService(mcpResourceUri).uninstall(server, options);676}677678async installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {679const mcpResourceUri = options?.mcpResource || this.userDataProfilesService.defaultProfile.mcpResource;680return this.getMcpResourceManagementService(mcpResourceUri).installFromGallery(server, options);681}682683async updateMetadata(local: ILocalMcpServer, gallery: IGalleryMcpServer, mcpResource?: URI): Promise<ILocalMcpServer> {684return this.getMcpResourceManagementService(mcpResource || this.userDataProfilesService.defaultProfile.mcpResource).updateMetadata(local, gallery);685}686687override dispose(): void {688this.mcpResourceManagementServices.forEach(service => service.dispose());689this.mcpResourceManagementServices.clear();690super.dispose();691}692693protected createMcpResourceManagementService(mcpResource: URI): McpUserResourceManagementService {694return this.instantiationService.createInstance(McpUserResourceManagementService, mcpResource);695}696697}698699700