Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpServerConnection.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 { 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 { IMcpClientMethods, IMcpServerConnection, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from './mcpTypes.js';1516export class McpServerConnection extends Disposable implements IMcpServerConnection {17private readonly _launch = this._register(new MutableDisposable<IReference<IMcpMessageTransport>>());18private readonly _state = observableValue<McpConnectionState>('mcpServerState', { state: McpConnectionState.Kind.Stopped });19private readonly _requestHandler = observableValue<McpServerRequestHandler | undefined>('mcpServerRequestHandler', undefined);2021public readonly state: IObservable<McpConnectionState> = this._state;22public readonly handler: IObservable<McpServerRequestHandler | undefined> = this._requestHandler;2324constructor(25private readonly _collection: McpCollectionDefinition,26public readonly definition: McpServerDefinition,27private readonly _delegate: IMcpHostDelegate,28public readonly launchDefinition: McpServerLaunch,29private readonly _logger: ILogger,30@IInstantiationService private readonly _instantiationService: IInstantiationService,31) {32super();33}3435/** @inheritdoc */36public async start(methods: IMcpClientMethods): Promise<McpConnectionState> {37const currentState = this._state.get();38if (!McpConnectionState.canBeStarted(currentState.state)) {39return this._waitForState(McpConnectionState.Kind.Running, McpConnectionState.Kind.Error);40}4142this._launch.value = undefined;43this._state.set({ state: McpConnectionState.Kind.Starting }, undefined);44this._logger.info(localize('mcpServer.starting', 'Starting server {0}', this.definition.label));4546try {47const launch = this._delegate.start(this._collection, this.definition, this.launchDefinition);48this._launch.value = this.adoptLaunch(launch, methods);49return this._waitForState(McpConnectionState.Kind.Running, McpConnectionState.Kind.Error);50} catch (e) {51const errorState: McpConnectionState = {52state: McpConnectionState.Kind.Error,53message: e instanceof Error ? e.message : String(e)54};55this._state.set(errorState, undefined);56return errorState;57}58}5960private adoptLaunch(launch: IMcpMessageTransport, methods: IMcpClientMethods): IReference<IMcpMessageTransport> {61const store = new DisposableStore();62const cts = new CancellationTokenSource();6364store.add(toDisposable(() => cts.dispose(true)));65store.add(launch);66store.add(launch.onDidLog(({ level, message }) => {67log(this._logger, level, message);68}));6970let didStart = false;71store.add(autorun(reader => {72const state = launch.state.read(reader);73this._state.set(state, undefined);74this._logger.info(localize('mcpServer.state', 'Connection state: {0}', McpConnectionState.toString(state)));7576if (state.state === McpConnectionState.Kind.Running && !didStart) {77didStart = true;78McpServerRequestHandler.create(this._instantiationService, {79launch,80logger: this._logger,81requestLogLevel: this.definition.devMode ? LogLevel.Info : LogLevel.Debug,82...methods,83}, cts.token).then(84handler => {85if (!store.isDisposed) {86this._requestHandler.set(handler, undefined);87} else {88handler.dispose();89}90},91err => {92if (!store.isDisposed) {93let message = err.message;94if (err instanceof CancellationError) {95message = 'Server exited before responding to `initialize` request.';96this._logger.error(message);97} else {98this._logger.error(err);99}100this._state.set({ state: McpConnectionState.Kind.Error, message }, undefined);101}102store.dispose();103},104);105}106}));107108return { dispose: () => store.dispose(), object: launch };109}110111public async stop(): Promise<void> {112this._logger.info(localize('mcpServer.stopping', 'Stopping server {0}', this.definition.label));113this._launch.value?.object.stop();114await this._waitForState(McpConnectionState.Kind.Stopped, McpConnectionState.Kind.Error);115}116117public override dispose(): void {118this._requestHandler.get()?.dispose();119super.dispose();120this._state.set({ state: McpConnectionState.Kind.Stopped }, undefined);121}122123private _waitForState(...kinds: McpConnectionState.Kind[]): Promise<McpConnectionState> {124const current = this._state.get();125if (kinds.includes(current.state)) {126return Promise.resolve(current);127}128129return new Promise(resolve => {130const disposable = autorun(reader => {131const state = this._state.read(reader);132if (kinds.includes(state.state)) {133disposable.dispose();134resolve(state);135}136});137});138}139}140141142