Path: blob/main/src/vs/workbench/contrib/mcp/browser/mcpMigration.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 { Disposable } from '../../../../base/common/lifecycle.js';6import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js';7import { ILogService } from '../../../../platform/log/common/log.js';8import { IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration, McpServerType } from '../../../../platform/mcp/common/mcpPlatformTypes.js';9import { IStringDictionary } from '../../../../base/common/collections.js';10import { mcpConfigurationSection } from '../../../contrib/mcp/common/mcpConfiguration.js';11import { IWorkbenchMcpManagementService } from '../../../services/mcp/common/mcpWorkbenchManagementService.js';12import { IWorkbenchContribution } from '../../../common/contributions.js';13import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';14import { FileOperationResult, IFileService, toFileOperationResult } from '../../../../platform/files/common/files.js';15import { URI } from '../../../../base/common/uri.js';16import { parse } from '../../../../base/common/jsonc.js';17import { isObject, Mutable } from '../../../../base/common/types.js';18import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';19import { IJSONEditingService } from '../../../services/configuration/common/jsonEditing.js';20import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';21import { ICommandService } from '../../../../platform/commands/common/commands.js';22import { McpCommandIds } from '../common/mcpCommandIds.js';23import { localize } from '../../../../nls.js';2425interface IMcpConfiguration {26inputs?: IMcpServerVariable[];27servers?: IStringDictionary<IMcpServerConfiguration>;28}2930export class McpConfigMigrationContribution extends Disposable implements IWorkbenchContribution {3132static ID = 'workbench.mcp.config.migration';3334constructor(35@IWorkbenchMcpManagementService private readonly mcpManagementService: IWorkbenchMcpManagementService,36@IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService,37@IFileService private readonly fileService: IFileService,38@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,39@IJSONEditingService private readonly jsonEditingService: IJSONEditingService,40@ILogService private readonly logService: ILogService,41@INotificationService private readonly notificationService: INotificationService,42@ICommandService private readonly commandService: ICommandService,43) {44super();45this.migrateMcpConfig();46}4748private async migrateMcpConfig(): Promise<void> {49try {50const userMcpConfig = await this.parseMcpConfig(this.userDataProfileService.currentProfile.settingsResource);51if (userMcpConfig && userMcpConfig.servers && Object.keys(userMcpConfig.servers).length > 0) {52await Promise.all(Object.entries(userMcpConfig.servers).map(([name, config], index) => this.mcpManagementService.install({ name, config, inputs: index === 0 ? userMcpConfig.inputs : undefined })));53await this.removeMcpConfig(this.userDataProfileService.currentProfile.settingsResource);54}55} catch (error) {56this.logService.error(`MCP migration: Failed to migrate user MCP config`, error);57}58this.watchForMcpConfiguration(this.userDataProfileService.currentProfile.settingsResource, false);5960const remoteEnvironment = await this.remoteAgentService.getEnvironment();61if (remoteEnvironment) {62try {63const userRemoteMcpConfig = await this.parseMcpConfig(remoteEnvironment.settingsPath);64if (userRemoteMcpConfig && userRemoteMcpConfig.servers && Object.keys(userRemoteMcpConfig.servers).length > 0) {65await Promise.all(Object.entries(userRemoteMcpConfig.servers).map(([name, config], index) => this.mcpManagementService.install({ name, config, inputs: index === 0 ? userRemoteMcpConfig.inputs : undefined }, { target: ConfigurationTarget.USER_REMOTE })));66await this.removeMcpConfig(remoteEnvironment.settingsPath);67}68} catch (error) {69this.logService.error(`MCP migration: Failed to migrate remote MCP config`, error);70}71this.watchForMcpConfiguration(remoteEnvironment.settingsPath, true);72}7374}7576private watchForMcpConfiguration(file: URI, isRemote: boolean): void {77this._register(this.fileService.watch(file));78this._register(this.fileService.onDidFilesChange(e => {79if (e.contains(file)) {80this.checkForMcpConfigInFile(file, isRemote);81}82}));83}8485private async checkForMcpConfigInFile(settingsFile: URI, isRemote: boolean): Promise<void> {86try {87const mcpConfig = await this.parseMcpConfig(settingsFile);88if (mcpConfig && mcpConfig.servers && Object.keys(mcpConfig.servers).length > 0) {89this.showMcpConfigErrorNotification(isRemote);90}91} catch (error) {92// Ignore parsing errors - file might not exist or be malformed93}94}9596private showMcpConfigErrorNotification(isRemote: boolean): void {97const message = isRemote98? localize('mcp.migration.remoteConfigFound', 'MCP servers should no longer be configured in remote user settings. Use the dedicated MCP configuration instead.')99: localize('mcp.migration.userConfigFound', 'MCP servers should no longer be configured in user settings. Use the dedicated MCP configuration instead.');100101const openConfigLabel = isRemote102? localize('mcp.migration.openRemoteConfig', 'Open Remote User MCP Configuration')103: localize('mcp.migration.openUserConfig', 'Open User MCP Configuration');104105const commandId = isRemote ? McpCommandIds.OpenRemoteUserMcp : McpCommandIds.OpenUserMcp;106107this.notificationService.prompt(108Severity.Error,109message,110[{111label: localize('mcp.migration.update', 'Update Now'),112run: async () => {113await this.migrateMcpConfig();114await this.commandService.executeCommand(commandId);115},116}, {117label: openConfigLabel,118keepOpen: true,119run: () => this.commandService.executeCommand(commandId)120}]121);122}123124private async parseMcpConfig(settingsFile: URI): Promise<IMcpConfiguration | undefined> {125try {126const content = await this.fileService.readFile(settingsFile);127const settingsObject: IStringDictionary<any> = parse(content.value.toString());128if (!isObject(settingsObject)) {129return undefined;130}131const mcpConfiguration = settingsObject[mcpConfigurationSection] as IMcpConfiguration;132if (mcpConfiguration && mcpConfiguration.servers) {133for (const [, config] of Object.entries(mcpConfiguration.servers)) {134if (config.type === undefined) {135(<Mutable<IMcpServerConfiguration>>config).type = (<IMcpStdioServerConfiguration>config).command ? McpServerType.LOCAL : McpServerType.REMOTE;136}137}138}139return mcpConfiguration;140} catch (error) {141if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {142this.logService.warn(`MCP migration: Failed to parse MCP config from ${settingsFile}:`, error);143}144return;145}146}147148private async removeMcpConfig(settingsFile: URI): Promise<void> {149try {150await this.jsonEditingService.write(settingsFile, [151{152path: [mcpConfigurationSection],153value: undefined154}155], true);156} catch (error) {157this.logService.warn(`MCP migration: Failed to remove MCP config from ${settingsFile}:`, error);158}159}160}161162163