Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpService.ts
5263 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 { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';7import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';8import { autorun, IObservable, ISettableObservable, observableValue, transaction } from '../../../../base/common/observable.js';9import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';10import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';11import { ILogService } from '../../../../platform/log/common/log.js';12import { mcpAutoStartConfig, McpAutoStartValue } from '../../../../platform/mcp/common/mcpManagement.js';13import { StorageScope } from '../../../../platform/storage/common/storage.js';14import { IMcpRegistry } from './mcpRegistryTypes.js';15import { McpServer, McpServerMetadataCache } from './mcpServer.js';16import { IAutostartResult, IMcpServer, IMcpService, McpCollectionDefinition, McpConnectionState, McpDefinitionReference, McpServerCacheState, McpServerDefinition, McpStartServerInteraction, McpToolName, UserInteractionRequiredError } from './mcpTypes.js';17import { startServerAndWaitForLiveTools } from './mcpTypesUtils.js';1819type IMcpServerRec = { object: IMcpServer; toolPrefix: string };2021export class McpService extends Disposable implements IMcpService {2223declare _serviceBrand: undefined;2425private readonly _currentAutoStarts = new Set<CancellationTokenSource>();26private readonly _servers = observableValue<readonly IMcpServerRec[]>(this, []);27public readonly servers: IObservable<readonly IMcpServer[]> = this._servers.map(servers => servers.map(s => s.object));2829public get lazyCollectionState() { return this._mcpRegistry.lazyCollectionState; }3031protected readonly userCache: McpServerMetadataCache;32protected readonly workspaceCache: McpServerMetadataCache;3334constructor(35@IInstantiationService private readonly _instantiationService: IInstantiationService,36@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,37@ILogService private readonly _logService: ILogService,38@IConfigurationService private readonly configurationService: IConfigurationService39) {40super();4142this.userCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.PROFILE));43this.workspaceCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.WORKSPACE));4445const updateThrottle = this._store.add(new RunOnceScheduler(() => this.updateCollectedServers(), 500));4647// Throttle changes so that if a collection is changed, or a server is48// unregistered/registered, we don't stop servers unnecessarily.49this._register(autorun(reader => {50for (const collection of this._mcpRegistry.collections.read(reader)) {51collection.serverDefinitions.read(reader);52}53updateThrottle.schedule(500);54}));55}5657public cancelAutostart(): void {58for (const cts of this._currentAutoStarts) {59cts.cancel();60}61}6263public autostart(_token?: CancellationToken): IObservable<IAutostartResult> {64const autoStartConfig = this.configurationService.getValue<McpAutoStartValue>(mcpAutoStartConfig);65if (autoStartConfig === McpAutoStartValue.Never) {66return observableValue<IAutostartResult>(this, IAutostartResult.Empty);67}6869const state = observableValue<IAutostartResult>(this, { working: true, starting: [], serversRequiringInteraction: [] });70const store = new DisposableStore();7172const cts = store.add(new CancellationTokenSource(_token));73this._currentAutoStarts.add(cts);74store.add(toDisposable(() => {75this._currentAutoStarts.delete(cts);76}));77store.add(cts.token.onCancellationRequested(() => {78state.set(IAutostartResult.Empty, undefined);79}));8081this._autostart(autoStartConfig, state, cts.token)82.catch(err => {83this._logService.error('Error during MCP autostart:', err);84state.set(IAutostartResult.Empty, undefined);85})86.finally(() => store.dispose());8788return state;89}9091private async _autostart(autoStartConfig: McpAutoStartValue, state: ISettableObservable<IAutostartResult>, token: CancellationToken) {92await this._activateCollections();9394if (token.isCancellationRequested) {95return;96}9798// don't try re-running errored servers, let the user choose if they want that99const candidates = this.servers.get().filter(s => s.connectionState.get().state !== McpConnectionState.Kind.Error);100101let todo = new Set<IMcpServer>();102if (autoStartConfig === McpAutoStartValue.OnlyNew) {103todo = new Set(candidates.filter(s => s.cacheState.get() === McpServerCacheState.Unknown));104} else if (autoStartConfig === McpAutoStartValue.NewAndOutdated) {105todo = new Set(candidates.filter(s => {106const c = s.cacheState.get();107return c === McpServerCacheState.Unknown || c === McpServerCacheState.Outdated;108}));109}110111if (!todo.size) {112state.set(IAutostartResult.Empty, undefined);113return;114}115116const interaction = new McpStartServerInteraction();117const requiringInteraction: (McpDefinitionReference & { errorMessage?: string })[] = [];118119const update = () => state.set({120working: todo.size > 0,121starting: [...todo].map(t => t.definition),122serversRequiringInteraction: requiringInteraction,123}, undefined);124125update();126127await Promise.all([...todo].map(async (server, i) => {128try {129await startServerAndWaitForLiveTools(server, { interaction, errorOnUserInteraction: true }, token);130} catch (error) {131if (error instanceof UserInteractionRequiredError) {132requiringInteraction.push({ id: server.definition.id, label: server.definition.label, errorMessage: error.message });133}134} finally {135todo.delete(server);136if (!token.isCancellationRequested) {137update();138}139}140}));141}142143public resetCaches(): void {144this.userCache.reset();145this.workspaceCache.reset();146}147148public resetTrust(): void {149this.resetCaches(); // same difference now150}151152public async activateCollections(): Promise<void> {153await this._activateCollections();154}155156private async _activateCollections() {157const collections = await this._mcpRegistry.discoverCollections();158this.updateCollectedServers();159return new Set(collections.map(c => c.id));160}161162public updateCollectedServers() {163const prefixGenerator = new McpPrefixGenerator();164const definitions = this._mcpRegistry.collections.get().flatMap(collectionDefinition =>165collectionDefinition.serverDefinitions.get().map(serverDefinition => {166const toolPrefix = prefixGenerator.generate(serverDefinition.label);167return { serverDefinition, collectionDefinition, toolPrefix };168})169);170171const nextDefinitions = new Set(definitions);172const currentServers = this._servers.get();173const nextServers: IMcpServerRec[] = [];174const pushMatch = (match: (typeof definitions)[0], rec: IMcpServerRec) => {175nextDefinitions.delete(match);176nextServers.push(rec);177const connection = rec.object.connection.get();178// if the definition was modified, stop the server; it'll be restarted again on-demand179if (connection && !McpServerDefinition.equals(connection.definition, match.serverDefinition)) {180rec.object.stop();181this._logService.debug(`MCP server ${rec.object.definition.id} stopped because the definition changed`);182}183};184185// Transfer over any servers that are still valid.186for (const server of currentServers) {187const match = definitions.find(d => defsEqual(server.object, d) && server.toolPrefix === d.toolPrefix);188if (match) {189pushMatch(match, server);190} else {191server.object.dispose();192}193}194195// Create any new servers that are needed.196for (const def of nextDefinitions) {197const object = this._instantiationService.createInstance(198McpServer,199def.collectionDefinition,200def.serverDefinition,201def.serverDefinition.roots,202!!def.collectionDefinition.lazy,203def.collectionDefinition.scope === StorageScope.WORKSPACE ? this.workspaceCache : this.userCache,204def.toolPrefix,205);206207nextServers.push({ object, toolPrefix: def.toolPrefix });208}209210transaction(tx => {211this._servers.set(nextServers, tx);212});213}214215public override dispose(): void {216this._servers.get().forEach(s => s.object.dispose());217super.dispose();218}219}220221function defsEqual(server: IMcpServer, def: { serverDefinition: McpServerDefinition; collectionDefinition: McpCollectionDefinition }) {222return server.collection.id === def.collectionDefinition.id && server.definition.id === def.serverDefinition.id;223}224225// Helper class for generating unique MCP tool prefixes226class McpPrefixGenerator {227private readonly seenPrefixes = new Set<string>();228229generate(label: string): string {230const baseToolPrefix = McpToolName.Prefix + label.toLowerCase().replace(/[^a-z0-9_.-]+/g, '_').slice(0, McpToolName.MaxPrefixLen - McpToolName.Prefix.length - 1);231let toolPrefix = baseToolPrefix + '_';232for (let i = 2; this.seenPrefixes.has(toolPrefix); i++) {233toolPrefix = baseToolPrefix + i + '_';234}235this.seenPrefixes.add(toolPrefix);236return toolPrefix;237}238}239240241