Path: blob/main/src/vs/platform/mcp/common/mcpResourceScannerService.ts
3294 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 { assertNever } from '../../../base/common/assert.js';6import { Queue } from '../../../base/common/async.js';7import { VSBuffer } from '../../../base/common/buffer.js';8import { IStringDictionary } from '../../../base/common/collections.js';9import { parse, ParseError } from '../../../base/common/json.js';10import { Disposable } from '../../../base/common/lifecycle.js';11import { ResourceMap } from '../../../base/common/map.js';12import { Mutable } from '../../../base/common/types.js';13import { URI } from '../../../base/common/uri.js';14import { ConfigurationTarget, ConfigurationTargetToString } from '../../configuration/common/configuration.js';15import { FileOperationResult, IFileService, toFileOperationResult } from '../../files/common/files.js';16import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';17import { createDecorator } from '../../instantiation/common/instantiation.js';18import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';19import { IInstallableMcpServer } from './mcpManagement.js';20import { ICommonMcpServerConfiguration, IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration, McpServerType } from './mcpPlatformTypes.js';2122interface IScannedMcpServers {23servers?: IStringDictionary<Mutable<IMcpServerConfiguration>>;24inputs?: IMcpServerVariable[];25}2627interface IOldScannedMcpServer {28id: string;29name: string;30version?: string;31gallery?: boolean;32config: Mutable<IMcpServerConfiguration>;33}3435interface IScannedWorkspaceMcpServers {36settings?: {37mcp?: IScannedMcpServers;38};39}4041export type McpResourceTarget = ConfigurationTarget.USER | ConfigurationTarget.WORKSPACE | ConfigurationTarget.WORKSPACE_FOLDER;4243export const IMcpResourceScannerService = createDecorator<IMcpResourceScannerService>('IMcpResourceScannerService');44export interface IMcpResourceScannerService {45readonly _serviceBrand: undefined;46scanMcpServers(mcpResource: URI, target?: McpResourceTarget): Promise<IScannedMcpServers>;47addMcpServers(servers: IInstallableMcpServer[], mcpResource: URI, target?: McpResourceTarget): Promise<void>;48removeMcpServers(serverNames: string[], mcpResource: URI, target?: McpResourceTarget): Promise<void>;49}5051export class McpResourceScannerService extends Disposable implements IMcpResourceScannerService {52readonly _serviceBrand: undefined;5354private readonly resourcesAccessQueueMap = new ResourceMap<Queue<IScannedMcpServers>>();5556constructor(57@IFileService private readonly fileService: IFileService,58@IUriIdentityService protected readonly uriIdentityService: IUriIdentityService,59) {60super();61}6263async scanMcpServers(mcpResource: URI, target?: McpResourceTarget): Promise<IScannedMcpServers> {64return this.withProfileMcpServers(mcpResource, target);65}6667async addMcpServers(servers: IInstallableMcpServer[], mcpResource: URI, target?: McpResourceTarget): Promise<void> {68await this.withProfileMcpServers(mcpResource, target, scannedMcpServers => {69let updatedInputs = scannedMcpServers.inputs ?? [];70const existingServers = scannedMcpServers.servers ?? {};71for (const { name, config, inputs } of servers) {72existingServers[name] = config;73if (inputs) {74const existingInputIds = new Set(updatedInputs.map(input => input.id));75const newInputs = inputs.filter(input => !existingInputIds.has(input.id));76updatedInputs = [...updatedInputs, ...newInputs];77}78}79return { servers: existingServers, inputs: updatedInputs };80});81}8283async removeMcpServers(serverNames: string[], mcpResource: URI, target?: McpResourceTarget): Promise<void> {84await this.withProfileMcpServers(mcpResource, target, scannedMcpServers => {85for (const serverName of serverNames) {86if (scannedMcpServers.servers?.[serverName]) {87delete scannedMcpServers.servers[serverName];88}89}90return scannedMcpServers;91});92}9394private async withProfileMcpServers(mcpResource: URI, target?: McpResourceTarget, updateFn?: (data: IScannedMcpServers) => IScannedMcpServers): Promise<IScannedMcpServers> {95return this.getResourceAccessQueue(mcpResource)96.queue(async (): Promise<IScannedMcpServers> => {97target = target ?? ConfigurationTarget.USER;98let scannedMcpServers: IScannedMcpServers = {};99try {100const content = await this.fileService.readFile(mcpResource);101const errors: ParseError[] = [];102const result = parse(content.value.toString(), errors, { allowTrailingComma: true, allowEmptyContent: true }) || {};103if (errors.length > 0) {104throw new Error('Failed to parse scanned MCP servers: ' + errors.join(', '));105}106107if (target === ConfigurationTarget.USER) {108scannedMcpServers = this.fromUserMcpServers(result);109} else if (target === ConfigurationTarget.WORKSPACE_FOLDER) {110scannedMcpServers = this.fromWorkspaceFolderMcpServers(result);111} else if (target === ConfigurationTarget.WORKSPACE) {112const workspaceScannedMcpServers: IScannedWorkspaceMcpServers = result;113if (workspaceScannedMcpServers.settings?.mcp) {114scannedMcpServers = this.fromWorkspaceFolderMcpServers(workspaceScannedMcpServers.settings?.mcp);115}116}117} catch (error) {118if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {119throw error;120}121}122if (updateFn) {123scannedMcpServers = updateFn(scannedMcpServers ?? {});124125if (target === ConfigurationTarget.USER) {126await this.writeScannedMcpServers(mcpResource, scannedMcpServers);127} else if (target === ConfigurationTarget.WORKSPACE_FOLDER) {128await this.writeScannedMcpServersToWorkspaceFolder(mcpResource, scannedMcpServers);129} else if (target === ConfigurationTarget.WORKSPACE) {130await this.writeScannedMcpServersToWorkspace(mcpResource, scannedMcpServers);131} else {132assertNever(target, `Invalid Target: ${ConfigurationTargetToString(target)}`);133}134}135return scannedMcpServers;136});137}138139private async writeScannedMcpServers(mcpResource: URI, scannedMcpServers: IScannedMcpServers): Promise<void> {140if ((scannedMcpServers.servers && Object.keys(scannedMcpServers.servers).length > 0) || (scannedMcpServers.inputs && scannedMcpServers.inputs.length > 0)) {141await this.fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify(scannedMcpServers, null, '\t')));142} else {143await this.fileService.del(mcpResource);144}145}146147private async writeScannedMcpServersToWorkspaceFolder(mcpResource: URI, scannedMcpServers: IScannedMcpServers): Promise<void> {148await this.fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify(scannedMcpServers, null, '\t')));149}150151private async writeScannedMcpServersToWorkspace(mcpResource: URI, scannedMcpServers: IScannedMcpServers): Promise<void> {152let scannedWorkspaceMcpServers: IScannedWorkspaceMcpServers | undefined;153try {154const content = await this.fileService.readFile(mcpResource);155const errors: ParseError[] = [];156scannedWorkspaceMcpServers = parse(content.value.toString(), errors, { allowTrailingComma: true, allowEmptyContent: true }) as IScannedWorkspaceMcpServers;157if (errors.length > 0) {158throw new Error('Failed to parse scanned MCP servers: ' + errors.join(', '));159}160} catch (error) {161if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {162throw error;163}164scannedWorkspaceMcpServers = { settings: {} };165}166if (!scannedWorkspaceMcpServers.settings) {167scannedWorkspaceMcpServers.settings = {};168}169scannedWorkspaceMcpServers.settings.mcp = scannedMcpServers;170await this.fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify(scannedWorkspaceMcpServers, null, '\t')));171}172173private fromUserMcpServers(scannedMcpServers: IScannedMcpServers): IScannedMcpServers {174const userMcpServers: IScannedMcpServers = {175inputs: scannedMcpServers.inputs176};177const servers = Object.entries(scannedMcpServers.servers ?? {});178if (servers.length > 0) {179userMcpServers.servers = {};180for (const [serverName, server] of servers) {181userMcpServers.servers[serverName] = this.sanitizeServer(server);182}183}184return userMcpServers;185}186187private fromWorkspaceFolderMcpServers(scannedWorkspaceFolderMcpServers: IScannedMcpServers): IScannedMcpServers {188const scannedMcpServers: IScannedMcpServers = {189inputs: scannedWorkspaceFolderMcpServers.inputs190};191const servers = Object.entries(scannedWorkspaceFolderMcpServers.servers ?? {});192if (servers.length > 0) {193scannedMcpServers.servers = {};194for (const [serverName, config] of servers) {195scannedMcpServers.servers[serverName] = this.sanitizeServer(config);196}197}198return scannedMcpServers;199}200201private sanitizeServer(serverOrConfig: IOldScannedMcpServer | Mutable<IMcpServerConfiguration>): IMcpServerConfiguration {202let server: IMcpServerConfiguration;203if ((<IOldScannedMcpServer>serverOrConfig).config) {204const oldScannedMcpServer = <IOldScannedMcpServer>serverOrConfig;205server = {206...oldScannedMcpServer.config,207version: oldScannedMcpServer.version,208gallery: oldScannedMcpServer.gallery209};210} else {211server = serverOrConfig as IMcpServerConfiguration;212}213214if (server.type === undefined || (server.type !== McpServerType.REMOTE && server.type !== McpServerType.LOCAL)) {215(<Mutable<ICommonMcpServerConfiguration>>server).type = (<IMcpStdioServerConfiguration>server).command ? McpServerType.LOCAL : McpServerType.REMOTE;216}217218return server;219}220221private getResourceAccessQueue(file: URI): Queue<IScannedMcpServers> {222let resourceQueue = this.resourcesAccessQueueMap.get(file);223if (!resourceQueue) {224resourceQueue = new Queue<IScannedMcpServers>();225this.resourcesAccessQueueMap.set(file, resourceQueue);226}227return resourceQueue;228}229}230231registerSingleton(IMcpResourceScannerService, McpResourceScannerService, InstantiationType.Delayed);232233234