Path: blob/main/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAbstract.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 { RunOnceScheduler } from '../../../../../base/common/async.js';6import { VSBuffer } from '../../../../../base/common/buffer.js';7import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';8import { Schemas } from '../../../../../base/common/network.js';9import { autorun, IObservable, IReader, ISettableObservable, observableValue } from '../../../../../base/common/observable.js';10import { URI } from '../../../../../base/common/uri.js';11import { localize } from '../../../../../nls.js';12import { ConfigurationTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';13import { IFileService } from '../../../../../platform/files/common/files.js';14import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';15import { ILabelService } from '../../../../../platform/label/common/label.js';16import { INativeMcpDiscoveryData } from '../../../../../platform/mcp/common/nativeMcpDiscoveryHelper.js';17import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js';18import { StorageScope } from '../../../../../platform/storage/common/storage.js';19import { Dto } from '../../../../services/extensions/common/proxyIdentifier.js';20import { DiscoverySource, discoverySourceLabel, mcpDiscoverySection } from '../mcpConfiguration.js';21import { IMcpRegistry } from '../mcpRegistryTypes.js';22import { McpCollectionDefinition, McpCollectionSortOrder, McpServerDefinition, McpServerTrust } from '../mcpTypes.js';23import { IMcpDiscovery } from './mcpDiscovery.js';24import { ClaudeDesktopMpcDiscoveryAdapter, CursorDesktopMpcDiscoveryAdapter, NativeMpcDiscoveryAdapter, WindsurfDesktopMpcDiscoveryAdapter } from './nativeMcpDiscoveryAdapters.js';2526export type WritableMcpCollectionDefinition = McpCollectionDefinition & { serverDefinitions: ISettableObservable<readonly McpServerDefinition[]> };2728export abstract class FilesystemMcpDiscovery extends Disposable implements IMcpDiscovery {2930readonly fromGallery: boolean = false;3132protected readonly _fsDiscoveryEnabled: IObservable<{ [K in DiscoverySource]: boolean } | undefined>;3334constructor(35@IConfigurationService configurationService: IConfigurationService,36@IFileService private readonly _fileService: IFileService,37@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,38) {39super();4041this._fsDiscoveryEnabled = observableConfigValue(mcpDiscoverySection, undefined, configurationService);42}4344protected _isDiscoveryEnabled(reader: IReader, discoverySource: DiscoverySource): boolean {45const fsDiscovery = this._fsDiscoveryEnabled.read(reader);46if (typeof fsDiscovery === 'boolean') {47return fsDiscovery; // old commands48}49if (discoverySource && fsDiscovery?.[discoverySource] === true) {50return true;51}52return false;53}5455protected watchFile(56file: URI,57collection: WritableMcpCollectionDefinition,58discoverySource: DiscoverySource,59adaptFile: (contents: VSBuffer) => Promise<McpServerDefinition[] | undefined>,60): IDisposable {61const store = new DisposableStore();62const collectionRegistration = store.add(new MutableDisposable());63const updateFile = async () => {64let definitions: McpServerDefinition[] = [];65try {66const contents = await this._fileService.readFile(file);67definitions = await adaptFile(contents.value) || [];68} catch {69// ignored70}71if (!definitions.length) {72collectionRegistration.clear();73} else {74collection.serverDefinitions.set(definitions, undefined);75if (!collectionRegistration.value) {76collectionRegistration.value = this._mcpRegistry.registerCollection(collection);77}78}79};8081store.add(autorun(reader => {82if (!this._isDiscoveryEnabled(reader, discoverySource)) {83collectionRegistration.clear();84return;85}8687const throttler = reader.store.add(new RunOnceScheduler(updateFile, 500));88const watcher = reader.store.add(this._fileService.createWatcher(file, { recursive: false, excludes: [] }));89reader.store.add(watcher.onDidChange(() => throttler.schedule()));90updateFile();91}));9293return store;94}9596public abstract start(): void;97}9899/**100* Base class that discovers MCP servers on a filesystem, outside of the ones101* defined in VS Code settings.102*/103export abstract class NativeFilesystemMcpDiscovery extends FilesystemMcpDiscovery implements IMcpDiscovery {104private readonly adapters: readonly NativeMpcDiscoveryAdapter[];105private suffix = '';106107constructor(108remoteAuthority: string | null,109@ILabelService labelService: ILabelService,110@IFileService fileService: IFileService,111@IInstantiationService instantiationService: IInstantiationService,112@IMcpRegistry mcpRegistry: IMcpRegistry,113@IConfigurationService configurationService: IConfigurationService,114) {115super(configurationService, fileService, mcpRegistry);116if (remoteAuthority) {117this.suffix = ' ' + localize('onRemoteLabel', ' on {0}', labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority));118}119120this.adapters = [121instantiationService.createInstance(ClaudeDesktopMpcDiscoveryAdapter, remoteAuthority),122instantiationService.createInstance(CursorDesktopMpcDiscoveryAdapter, remoteAuthority),123instantiationService.createInstance(WindsurfDesktopMpcDiscoveryAdapter, remoteAuthority),124];125}126127protected setDetails(detailsDto: Dto<INativeMcpDiscoveryData> | undefined) {128if (!detailsDto) {129return;130}131132const details: INativeMcpDiscoveryData = {133...detailsDto,134homedir: URI.revive(detailsDto.homedir),135xdgHome: detailsDto.xdgHome ? URI.revive(detailsDto.xdgHome) : undefined,136winAppData: detailsDto.winAppData ? URI.revive(detailsDto.winAppData) : undefined,137};138139for (const adapter of this.adapters) {140const file = adapter.getFilePath(details);141if (!file) {142continue;143}144145const collection: WritableMcpCollectionDefinition = {146id: adapter.id,147label: discoverySourceLabel[adapter.discoverySource] + this.suffix,148remoteAuthority: adapter.remoteAuthority,149configTarget: ConfigurationTarget.USER,150scope: StorageScope.PROFILE,151trustBehavior: McpServerTrust.Kind.TrustedOnNonce,152serverDefinitions: observableValue<readonly McpServerDefinition[]>(this, []),153presentation: {154origin: file,155order: adapter.order + (adapter.remoteAuthority ? McpCollectionSortOrder.RemoteBoost : 0),156},157};158159this._register(this.watchFile(file, collection, adapter.discoverySource, contents => adapter.adaptFile(contents, details)));160}161}162}163164165