Path: blob/main/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts
5363 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 { equals } from '../../../../../base/common/arrays.js';6import { Throttler } from '../../../../../base/common/async.js';7import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';8import { ResourceMap } from '../../../../../base/common/map.js';9import { ISettableObservable, observableValue } from '../../../../../base/common/observable.js';10import { URI } from '../../../../../base/common/uri.js';11import { Location } from '../../../../../editor/common/languages.js';12import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';13import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js';14import { ILogService } from '../../../../../platform/log/common/log.js';15import { StorageScope } from '../../../../../platform/storage/common/storage.js';16import { IWorkbenchLocalMcpServer } from '../../../../services/mcp/common/mcpWorkbenchManagementService.js';17import { getMcpServerMapping } from '../mcpConfigFileUtils.js';18import { mcpConfigurationSection } from '../mcpConfiguration.js';19import { IMcpRegistry } from '../mcpRegistryTypes.js';20import { IMcpConfigPath, IMcpWorkbenchService, McpCollectionDefinition, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust } from '../mcpTypes.js';21import { IMcpDiscovery } from './mcpDiscovery.js';2223interface CollectionState extends IDisposable {24definition: McpCollectionDefinition;25serverDefinitions: ISettableObservable<readonly McpServerDefinition[]>;26}2728export class InstalledMcpServersDiscovery extends Disposable implements IMcpDiscovery {2930readonly fromGallery = true;31private readonly collections = this._register(new DisposableMap<string, CollectionState>());3233constructor(34@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,35@IMcpRegistry private readonly mcpRegistry: IMcpRegistry,36@ITextModelService private readonly textModelService: ITextModelService,37@ILogService private readonly logService: ILogService,38) {39super();40}4142public start(): void {43const throttler = this._register(new Throttler());44this._register(this.mcpWorkbenchService.onChange(() => throttler.queue(() => this.sync())));45this.sync();46}4748private async getServerIdMapping(resource: URI, pathToServers: string[]): Promise<Map<string, Location>> {49const store = new DisposableStore();50try {51const ref = await this.textModelService.createModelReference(resource);52store.add(ref);53const serverIdMapping = getMcpServerMapping({ model: ref.object.textEditorModel, pathToServers });54return serverIdMapping;55} catch {56return new Map();57} finally {58store.dispose();59}60}6162private async sync(): Promise<void> {63try {64const collections = new Map<string, [IMcpConfigPath | undefined, McpServerDefinition[]]>();65const mcpConfigPathInfos = new ResourceMap<Promise<IMcpConfigPath & { locations: Map<string, Location> } | undefined>>();66for (const server of this.mcpWorkbenchService.getEnabledLocalMcpServers()) {67let mcpConfigPathPromise = mcpConfigPathInfos.get(server.mcpResource);68if (!mcpConfigPathPromise) {69mcpConfigPathPromise = (async (local: IWorkbenchLocalMcpServer) => {70const mcpConfigPath = this.mcpWorkbenchService.getMcpConfigPath(local);71const locations = mcpConfigPath?.uri ? await this.getServerIdMapping(mcpConfigPath?.uri, mcpConfigPath.section ? [...mcpConfigPath.section, 'servers'] : ['servers']) : new Map();72return mcpConfigPath ? { ...mcpConfigPath, locations } : undefined;73})(server);74mcpConfigPathInfos.set(server.mcpResource, mcpConfigPathPromise);75}7677const config = server.config;78const mcpConfigPath = await mcpConfigPathPromise;79const collectionId = `mcp.config.${mcpConfigPath ? mcpConfigPath.id : 'unknown'}`;8081let definitions = collections.get(collectionId);82if (!definitions) {83definitions = [mcpConfigPath, []];84collections.set(collectionId, definitions);85}8687const launch: McpServerLaunch = config.type === 'http' ? {88type: McpServerTransportType.HTTP,89uri: URI.parse(config.url),90headers: Object.entries(config.headers || {}),91} : {92type: McpServerTransportType.Stdio,93command: config.command,94args: config.args || [],95env: config.env || {},96envFile: config.envFile,97cwd: config.cwd,98};99100definitions[1].push({101id: `${collectionId}.${server.name}`,102label: server.name,103launch,104cacheNonce: await McpServerLaunch.hash(launch),105roots: mcpConfigPath?.workspaceFolder ? [mcpConfigPath.workspaceFolder.uri] : undefined,106variableReplacement: {107folder: mcpConfigPath?.workspaceFolder,108section: mcpConfigurationSection,109target: mcpConfigPath?.target ?? ConfigurationTarget.USER,110},111devMode: config.dev,112presentation: {113order: mcpConfigPath?.order,114origin: mcpConfigPath?.locations.get(server.name)115}116});117}118119for (const [id] of this.collections) {120if (!collections.has(id)) {121this.collections.deleteAndDispose(id);122}123}124125for (const [id, [mcpConfigPath, serverDefinitions]] of collections) {126const newServerDefinitions = observableValue<readonly McpServerDefinition[]>(this, serverDefinitions);127const newCollection: McpCollectionDefinition = {128id,129label: mcpConfigPath?.label ?? '',130presentation: {131order: serverDefinitions[0]?.presentation?.order,132origin: mcpConfigPath?.uri,133},134remoteAuthority: mcpConfigPath?.remoteAuthority ?? null,135serverDefinitions: newServerDefinitions,136trustBehavior: McpServerTrust.Kind.Trusted,137configTarget: mcpConfigPath?.target ?? ConfigurationTarget.USER,138scope: mcpConfigPath?.scope ?? StorageScope.PROFILE,139};140const existingCollection = this.collections.get(id);141142const collectionDefinitionsChanged = existingCollection ? !McpCollectionDefinition.equals(existingCollection.definition, newCollection) : true;143if (!collectionDefinitionsChanged) {144const serverDefinitionsChanged = existingCollection ? !equals(existingCollection.definition.serverDefinitions.get(), newCollection.serverDefinitions.get(), McpServerDefinition.equals) : true;145if (serverDefinitionsChanged) {146existingCollection?.serverDefinitions.set(serverDefinitions, undefined);147}148continue;149}150151this.collections.deleteAndDispose(id);152const disposable = this.mcpRegistry.registerCollection(newCollection);153this.collections.set(id, {154definition: newCollection,155serverDefinitions: newServerDefinitions,156dispose: () => disposable.dispose()157});158}159160} catch (error) {161this.logService.error(error);162}163}164}165166167