Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpService.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 { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';7import { Disposable } from '../../../../base/common/lifecycle.js';8import { autorun, IObservable, observableValue, transaction } from '../../../../base/common/observable.js';9import { localize } from '../../../../nls.js';10import { ICommandService } from '../../../../platform/commands/common/commands.js';11import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';12import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';13import { ILogService } from '../../../../platform/log/common/log.js';14import { mcpAutoStartConfig, McpAutoStartValue } from '../../../../platform/mcp/common/mcpManagement.js';15import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';16import { StorageScope } from '../../../../platform/storage/common/storage.js';17import { IMcpRegistry } from './mcpRegistryTypes.js';18import { McpServer, McpServerMetadataCache } from './mcpServer.js';19import { IMcpServer, IMcpService, McpCollectionDefinition, McpConnectionState, McpServerCacheState, McpServerDefinition, McpStartServerInteraction, McpToolName } from './mcpTypes.js';20import { startServerAndWaitForLiveTools } from './mcpTypesUtils.js';2122type IMcpServerRec = { object: IMcpServer; toolPrefix: string };2324export class McpService extends Disposable implements IMcpService {2526declare _serviceBrand: undefined;2728private readonly _servers = observableValue<readonly IMcpServerRec[]>(this, []);29public readonly servers: IObservable<readonly IMcpServer[]> = this._servers.map(servers => servers.map(s => s.object));3031public get lazyCollectionState() { return this._mcpRegistry.lazyCollectionState; }3233protected readonly userCache: McpServerMetadataCache;34protected readonly workspaceCache: McpServerMetadataCache;3536constructor(37@IInstantiationService private readonly _instantiationService: IInstantiationService,38@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,39@ILogService private readonly _logService: ILogService,40@IProgressService private readonly progressService: IProgressService,41@ICommandService private readonly commandService: ICommandService,42@IConfigurationService private readonly configurationService: IConfigurationService43) {44super();4546this.userCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.PROFILE));47this.workspaceCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.WORKSPACE));4849const updateThrottle = this._store.add(new RunOnceScheduler(() => this.updateCollectedServers(), 500));5051// Throttle changes so that if a collection is changed, or a server is52// unregistered/registered, we don't stop servers unnecessarily.53this._register(autorun(reader => {54for (const collection of this._mcpRegistry.collections.read(reader)) {55collection.serverDefinitions.read(reader);56}57updateThrottle.schedule(500);58}));59}6061public async autostart(token?: CancellationToken): Promise<void> {62const autoStartConfig = this.configurationService.getValue<McpAutoStartValue>(mcpAutoStartConfig);6364// don't try re-running errored servers, let the user choose if they want that65const candidates = this.servers.get().filter(s => s.connectionState.get().state !== McpConnectionState.Kind.Error);6667let todo: IMcpServer[] = [];68if (autoStartConfig === McpAutoStartValue.OnlyNew) {69todo = candidates.filter(s => s.cacheState.get() === McpServerCacheState.Unknown);70} else if (autoStartConfig === McpAutoStartValue.NewAndOutdated) {71todo = candidates.filter(s => {72const c = s.cacheState.get();73return c === McpServerCacheState.Unknown || c === McpServerCacheState.Outdated;74});75}7677if (!todo.length) {78return;79}8081const interaction = new McpStartServerInteraction();82const cts = new CancellationTokenSource(token);8384await this.progressService.withProgress(85{86location: ProgressLocation.Notification,87cancellable: true,88delay: 5_000,89total: todo.length,90buttons: [91localize('mcp.autostart.send', 'Skip Waiting'),92localize('mcp.autostart.configure', 'Configure'),93]94},95report => {96const remaining = new Set(todo);97const doReport = () => report.report({ message: localize('mcp.autostart.progress', 'Waiting for MCP server "{0}" to start...', [...remaining].map(r => r.definition.label).join('", "')), total: todo.length, increment: 1 });98doReport();99return Promise.all(todo.map(async server => {100await startServerAndWaitForLiveTools(server, { interaction }, cts.token);101remaining.delete(server);102doReport();103}));104},105btn => {106if (btn === 1) {107this.commandService.executeCommand('workbench.action.openSettings', mcpAutoStartConfig);108}109cts.cancel();110},111);112113cts.dispose();114}115116public resetCaches(): void {117this.userCache.reset();118this.workspaceCache.reset();119}120121public resetTrust(): void {122this.resetCaches(); // same difference now123}124125public async activateCollections(): Promise<void> {126const collections = await this._mcpRegistry.discoverCollections();127const collectionIds = new Set(collections.map(c => c.id));128129this.updateCollectedServers();130131// Discover any newly-collected servers with unknown tools132const todo: Promise<unknown>[] = [];133for (const { object: server } of this._servers.get()) {134if (collectionIds.has(server.collection.id)) {135const state = server.cacheState.get();136if (state === McpServerCacheState.Unknown) {137todo.push(server.start());138}139}140}141142await Promise.all(todo);143}144145public updateCollectedServers() {146const prefixGenerator = new McpPrefixGenerator();147const definitions = this._mcpRegistry.collections.get().flatMap(collectionDefinition =>148collectionDefinition.serverDefinitions.get().map(serverDefinition => {149const toolPrefix = prefixGenerator.generate(serverDefinition.label);150return { serverDefinition, collectionDefinition, toolPrefix };151})152);153154const nextDefinitions = new Set(definitions);155const currentServers = this._servers.get();156const nextServers: IMcpServerRec[] = [];157const pushMatch = (match: (typeof definitions)[0], rec: IMcpServerRec) => {158nextDefinitions.delete(match);159nextServers.push(rec);160const connection = rec.object.connection.get();161// if the definition was modified, stop the server; it'll be restarted again on-demand162if (connection && !McpServerDefinition.equals(connection.definition, match.serverDefinition)) {163rec.object.stop();164this._logService.debug(`MCP server ${rec.object.definition.id} stopped because the definition changed`);165}166};167168// Transfer over any servers that are still valid.169for (const server of currentServers) {170const match = definitions.find(d => defsEqual(server.object, d) && server.toolPrefix === d.toolPrefix);171if (match) {172pushMatch(match, server);173} else {174server.object.dispose();175}176}177178// Create any new servers that are needed.179for (const def of nextDefinitions) {180const object = this._instantiationService.createInstance(181McpServer,182def.collectionDefinition,183def.serverDefinition,184def.serverDefinition.roots,185!!def.collectionDefinition.lazy,186def.collectionDefinition.scope === StorageScope.WORKSPACE ? this.workspaceCache : this.userCache,187def.toolPrefix,188);189190nextServers.push({ object, toolPrefix: def.toolPrefix });191}192193transaction(tx => {194this._servers.set(nextServers, tx);195});196}197198public override dispose(): void {199this._servers.get().forEach(s => s.object.dispose());200super.dispose();201}202}203204function defsEqual(server: IMcpServer, def: { serverDefinition: McpServerDefinition; collectionDefinition: McpCollectionDefinition }) {205return server.collection.id === def.collectionDefinition.id && server.definition.id === def.serverDefinition.id;206}207208// Helper class for generating unique MCP tool prefixes209class McpPrefixGenerator {210private readonly seenPrefixes = new Set<string>();211212generate(label: string): string {213const baseToolPrefix = McpToolName.Prefix + label.toLowerCase().replace(/[^a-z0-9_.-]+/g, '_').slice(0, McpToolName.MaxPrefixLen - McpToolName.Prefix.length - 1);214let toolPrefix = baseToolPrefix + '_';215for (let i = 2; this.seenPrefixes.has(toolPrefix); i++) {216toolPrefix = baseToolPrefix + i + '_';217}218this.seenPrefixes.add(toolPrefix);219return toolPrefix;220}221}222223224