Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpServerConnection.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 { CancellationTokenSource } from '../../../../base/common/cancellation.js';6import { CancellationError } from '../../../../base/common/errors.js';7import { Disposable, DisposableStore, IReference, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';8import { autorun, IObservable, observableValue } from '../../../../base/common/observable.js';9import { localize } from '../../../../nls.js';10import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';11import { ILogger, log, LogLevel } from '../../../../platform/log/common/log.js';12import { IMcpHostDelegate, IMcpMessageTransport } from './mcpRegistryTypes.js';13import { McpServerRequestHandler } from './mcpServerRequestHandler.js';14import { McpTaskManager } from './mcpTaskManager.js';15import { IMcpClientMethods, IMcpServerConnection, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from './mcpTypes.js';1617export class McpServerConnection extends Disposable implements IMcpServerConnection {18private readonly _launch = this._register(new MutableDisposable<IReference<IMcpMessageTransport>>());19private readonly _state = observableValue<McpConnectionState>('mcpServerState', { state: McpConnectionState.Kind.Stopped });20private readonly _requestHandler = observableValue<McpServerRequestHandler | undefined>('mcpServerRequestHandler', undefined);2122public readonly state: IObservable<McpConnectionState> = this._state;23public readonly handler: IObservable<McpServerRequestHandler | undefined> = this._requestHandler;2425constructor(26private readonly _collection: McpCollectionDefinition,27public readonly definition: McpServerDefinition,28private readonly _delegate: IMcpHostDelegate,29public readonly launchDefinition: McpServerLaunch,30private readonly _logger: ILogger,31private readonly _errorOnUserInteraction: boolean | undefined,32private readonly _taskManager: McpTaskManager,33@IInstantiationService private readonly _instantiationService: IInstantiationService,34) {35super();36}3738/** @inheritdoc */39public async start(methods: IMcpClientMethods): Promise<McpConnectionState> {40const currentState = this._state.get();41if (!McpConnectionState.canBeStarted(currentState.state)) {42return this._waitForState(McpConnectionState.Kind.Running, McpConnectionState.Kind.Error);43}4445this._launch.value = undefined;46this._state.set({ state: McpConnectionState.Kind.Starting }, undefined);47this._logger.info(localize('mcpServer.starting', 'Starting server {0}', this.definition.label));4849try {50const launch = this._delegate.start(this._collection, this.definition, this.launchDefinition, { errorOnUserInteraction: this._errorOnUserInteraction });51this._launch.value = this.adoptLaunch(launch, methods);52return this._waitForState(McpConnectionState.Kind.Running, McpConnectionState.Kind.Error);53} catch (e) {54const errorState: McpConnectionState = {55state: McpConnectionState.Kind.Error,56message: e instanceof Error ? e.message : String(e)57};58this._state.set(errorState, undefined);59return errorState;60}61}6263private adoptLaunch(launch: IMcpMessageTransport, methods: IMcpClientMethods): IReference<IMcpMessageTransport> {64const store = new DisposableStore();65const cts = new CancellationTokenSource();6667store.add(toDisposable(() => cts.dispose(true)));68store.add(launch);69store.add(launch.onDidLog(({ level, message }) => {70log(this._logger, level, message);71}));7273let didStart = false;74store.add(autorun(reader => {75const state = launch.state.read(reader);76this._state.set(state, undefined);77this._logger.info(localize('mcpServer.state', 'Connection state: {0}', McpConnectionState.toString(state)));7879if (state.state === McpConnectionState.Kind.Running && !didStart) {80didStart = true;81McpServerRequestHandler.create(this._instantiationService, {82...methods,83launch,84logger: this._logger,85requestLogLevel: this.definition.devMode ? LogLevel.Info : LogLevel.Debug,86taskManager: this._taskManager,87}, cts.token).then(88handler => {89if (!store.isDisposed) {90this._requestHandler.set(handler, undefined);91} else {92handler.dispose();93}94},95err => {96if (!store.isDisposed && McpConnectionState.isRunning(this._state.read(undefined))) {97let message = err.message;98if (err instanceof CancellationError) {99message = 'Server exited before responding to `initialize` request.';100this._logger.error(message);101} else {102this._logger.error(err);103}104this._state.set({ state: McpConnectionState.Kind.Error, message }, undefined);105}106store.dispose();107},108);109}110}));111112return { dispose: () => store.dispose(), object: launch };113}114115public async stop(): Promise<void> {116this._logger.info(localize('mcpServer.stopping', 'Stopping server {0}', this.definition.label));117this._launch.value?.object.stop();118await this._waitForState(McpConnectionState.Kind.Stopped, McpConnectionState.Kind.Error);119}120121public override dispose(): void {122this._requestHandler.get()?.dispose();123super.dispose();124this._state.set({ state: McpConnectionState.Kind.Stopped }, undefined);125}126127private _waitForState(...kinds: McpConnectionState.Kind[]): Promise<McpConnectionState> {128const current = this._state.get();129if (kinds.includes(current.state)) {130return Promise.resolve(current);131}132133return new Promise(resolve => {134const disposable = autorun(reader => {135const state = this._state.read(reader);136if (kinds.includes(state.state)) {137disposable.dispose();138resolve(state);139}140});141});142}143}144145146