Path: blob/main/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.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 { Throttler } from '../../../../../base/common/async.js';6import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';7import { ResourceMap } from '../../../../../base/common/map.js';8import { observableValue } from '../../../../../base/common/observable.js';9import { posix as pathPosix, sep as pathSep, win32 as pathWin32 } from '../../../../../base/common/path.js';10import { isWindows, OperatingSystem } from '../../../../../base/common/platform.js';11import { URI } from '../../../../../base/common/uri.js';12import { Location } from '../../../../../editor/common/languages.js';13import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';14import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js';15import { StorageScope } from '../../../../../platform/storage/common/storage.js';16import { IWorkbenchLocalMcpServer } from '../../../../services/mcp/common/mcpWorkbenchManagementService.js';17import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js';18import { getMcpServerMapping } from '../mcpConfigFileUtils.js';19import { mcpConfigurationSection } from '../mcpConfiguration.js';20import { IMcpRegistry } from '../mcpRegistryTypes.js';21import { IMcpConfigPath, IMcpWorkbenchService, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust } from '../mcpTypes.js';22import { IMcpDiscovery } from './mcpDiscovery.js';2324export class InstalledMcpServersDiscovery extends Disposable implements IMcpDiscovery {2526readonly fromGallery = true;27private readonly collectionDisposables = this._register(new DisposableMap<string, IDisposable>());2829constructor(30@IMcpWorkbenchService private readonly mcpWorkbenchService: IMcpWorkbenchService,31@IMcpRegistry private readonly mcpRegistry: IMcpRegistry,32@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,33@ITextModelService private readonly textModelService: ITextModelService,34) {35super();36}3738public start(): void {39const throttler = this._register(new Throttler());40this._register(this.mcpWorkbenchService.onChange(() => throttler.queue(() => this.sync())));41this.sync();42}4344private async getServerIdMapping(resource: URI, pathToServers: string[]): Promise<Map<string, Location>> {45const store = new DisposableStore();46try {47const ref = await this.textModelService.createModelReference(resource);48store.add(ref);49const serverIdMapping = getMcpServerMapping({ model: ref.object.textEditorModel, pathToServers });50return serverIdMapping;51} catch {52return new Map();53} finally {54store.dispose();55}56}5758private async sync(): Promise<void> {59try {60const remoteEnv = await this.remoteAgentService.getEnvironment();61const collections = new Map<string, [IMcpConfigPath | undefined, McpServerDefinition[]]>();62const mcpConfigPathInfos = new ResourceMap<Promise<IMcpConfigPath & { locations: Map<string, Location> } | undefined>>();63for (const server of this.mcpWorkbenchService.getEnabledLocalMcpServers()) {64let mcpConfigPathPromise = mcpConfigPathInfos.get(server.mcpResource);65if (!mcpConfigPathPromise) {66mcpConfigPathPromise = (async (local: IWorkbenchLocalMcpServer) => {67const mcpConfigPath = this.mcpWorkbenchService.getMcpConfigPath(local);68const locations = mcpConfigPath?.uri ? await this.getServerIdMapping(mcpConfigPath?.uri, mcpConfigPath.section ? [...mcpConfigPath.section, 'servers'] : ['servers']) : new Map();69return mcpConfigPath ? { ...mcpConfigPath, locations } : undefined;70})(server);71mcpConfigPathInfos.set(server.mcpResource, mcpConfigPathPromise);72}7374const config = server.config;75const mcpConfigPath = await mcpConfigPathPromise;76const collectionId = `mcp.config.${mcpConfigPath ? mcpConfigPath.id : 'unknown'}`;7778let definitions = collections.get(collectionId);79if (!definitions) {80definitions = [mcpConfigPath, []];81collections.set(collectionId, definitions);82}8384const { isAbsolute, join, sep } = mcpConfigPath?.remoteAuthority && remoteEnv85? (remoteEnv.os === OperatingSystem.Windows ? pathWin32 : pathPosix)86: (isWindows ? pathWin32 : pathPosix);87const fsPathForRemote = (uri: URI) => {88const fsPathLocal = uri.fsPath;89return fsPathLocal.replaceAll(pathSep, sep);90};9192const launch: McpServerLaunch = config.type === 'http' ? {93type: McpServerTransportType.HTTP,94uri: URI.parse(config.url),95headers: Object.entries(config.headers || {}),96} : {97type: McpServerTransportType.Stdio,98command: config.command,99args: config.args || [],100env: config.env || {},101envFile: config.envFile,102cwd: config.cwd103// if the cwd is defined in a workspace folder but not absolute (and not104// a variable or tilde-expansion) then resolve it in the workspace folder105// if the cwd is defined in a workspace folder but not absolute (and not106// a variable or tilde-expansion) then resolve it in the workspace folder107? (!isAbsolute(config.cwd) && !config.cwd.startsWith('~') && !config.cwd.startsWith('${') && mcpConfigPath?.workspaceFolder108? join(fsPathForRemote(mcpConfigPath.workspaceFolder.uri), config.cwd)109: config.cwd)110: mcpConfigPath?.workspaceFolder111? fsPathForRemote(mcpConfigPath.workspaceFolder.uri)112: undefined,113};114115definitions[1].push({116id: `${collectionId}.${server.name}`,117label: server.name,118launch,119cacheNonce: await McpServerLaunch.hash(launch),120roots: mcpConfigPath?.workspaceFolder ? [mcpConfigPath.workspaceFolder.uri] : undefined,121variableReplacement: {122folder: mcpConfigPath?.workspaceFolder,123section: mcpConfigurationSection,124target: mcpConfigPath?.target ?? ConfigurationTarget.USER,125},126devMode: config.dev,127presentation: {128order: mcpConfigPath?.order,129origin: mcpConfigPath?.locations.get(server.name)130}131});132}133134for (const [id, [mcpConfigPath, serverDefinitions]] of collections) {135this.collectionDisposables.deleteAndDispose(id);136this.collectionDisposables.set(id, this.mcpRegistry.registerCollection({137id,138label: mcpConfigPath?.label ?? '',139presentation: {140order: serverDefinitions[0]?.presentation?.order,141origin: mcpConfigPath?.uri,142},143remoteAuthority: mcpConfigPath?.remoteAuthority ?? null,144serverDefinitions: observableValue(this, serverDefinitions),145trustBehavior: mcpConfigPath?.workspaceFolder ? McpServerTrust.Kind.TrustedOnNonce : McpServerTrust.Kind.Trusted,146configTarget: mcpConfigPath?.target ?? ConfigurationTarget.USER,147scope: mcpConfigPath?.scope ?? StorageScope.PROFILE,148}));149}150for (const [id] of this.collectionDisposables) {151if (!collections.has(id)) {152this.collectionDisposables.deleteAndDispose(id);153}154}155156} catch (error) {157this.collectionDisposables.clearAndDisposeAll();158}159}160}161162163