Path: blob/main/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.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 * as assert from 'assert';6import { timeout } from '../../../../../base/common/async.js';7import { Disposable } from '../../../../../base/common/lifecycle.js';8import { autorun, observableValue } from '../../../../../base/common/observable.js';9import { upcast } from '../../../../../base/common/types.js';10import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';11import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';12import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';13import { ILogger, ILoggerService, LogLevel, NullLogger } from '../../../../../platform/log/common/log.js';14import { IProductService } from '../../../../../platform/product/common/productService.js';15import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js';16import { IOutputService } from '../../../../services/output/common/output.js';17import { TestLoggerService, TestProductService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';18import { IMcpHostDelegate, IMcpMessageTransport } from '../../common/mcpRegistryTypes.js';19import { McpServerConnection } from '../../common/mcpServerConnection.js';20import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust } from '../../common/mcpTypes.js';21import { TestMcpMessageTransport } from './mcpRegistryTypes.js';22import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js';23import { Event } from '../../../../../base/common/event.js';24import { McpTaskManager } from '../../common/mcpTaskManager.js';2526class TestMcpHostDelegate extends Disposable implements IMcpHostDelegate {27private readonly _transport: TestMcpMessageTransport;28private _canStartValue = true;2930priority = 0;3132constructor() {33super();34this._transport = this._register(new TestMcpMessageTransport());35}3637substituteVariables(serverDefinition: McpServerDefinition, launch: McpServerLaunch): Promise<McpServerLaunch> {38return Promise.resolve(launch);39}4041canStart(): boolean {42return this._canStartValue;43}4445start(): IMcpMessageTransport {46if (!this._canStartValue) {47throw new Error('Cannot start server');48}49return this._transport;50}5152getTransport(): TestMcpMessageTransport {53return this._transport;54}5556setCanStart(value: boolean): void {57this._canStartValue = value;58}5960waitForInitialProviderPromises(): Promise<void> {61return Promise.resolve();62}63}6465suite('Workbench - MCP - ServerConnection', () => {66const store = ensureNoDisposablesAreLeakedInTestSuite();6768let instantiationService: TestInstantiationService;69let delegate: TestMcpHostDelegate;70let transport: TestMcpMessageTransport;71let collection: McpCollectionDefinition;72let serverDefinition: McpServerDefinition;7374setup(() => {75delegate = store.add(new TestMcpHostDelegate());76transport = delegate.getTransport();7778// Setup test services79const services = new ServiceCollection(80[ILoggerService, store.add(new TestLoggerService())],81[IOutputService, upcast({ showChannel: () => { } })],82[IStorageService, store.add(new TestStorageService())],83[IProductService, TestProductService],84);8586instantiationService = store.add(new TestInstantiationService(services));8788// Create test collection89collection = {90id: 'test-collection',91label: 'Test Collection',92remoteAuthority: null,93serverDefinitions: observableValue('serverDefs', []),94trustBehavior: McpServerTrust.Kind.Trusted,95scope: StorageScope.APPLICATION,96configTarget: ConfigurationTarget.USER,97};9899// Create server definition100serverDefinition = {101id: 'test-server',102label: 'Test Server',103cacheNonce: 'a',104launch: {105type: McpServerTransportType.Stdio,106command: 'test-command',107args: [],108env: {},109envFile: undefined,110cwd: '/test'111}112};113});114115function waitForHandler(cnx: McpServerConnection) {116const handler = cnx.handler.get();117if (handler) {118return Promise.resolve(handler);119}120121return new Promise(resolve => {122const disposable = autorun(reader => {123const handler = cnx.handler.read(reader);124if (handler) {125disposable.dispose();126resolve(handler);127}128});129});130}131132test('should start and set state to Running when transport succeeds', async () => {133// Create server connection134const connection = instantiationService.createInstance(135McpServerConnection,136collection,137serverDefinition,138delegate,139serverDefinition.launch,140new NullLogger(),141false,142store.add(new McpTaskManager()),143);144store.add(connection);145146// Start the connection147const startPromise = connection.start({});148149// Simulate successful connection150transport.setConnectionState({ state: McpConnectionState.Kind.Running });151152const state = await startPromise;153assert.strictEqual(state.state, McpConnectionState.Kind.Running);154155transport.simulateInitialized();156assert.ok(await waitForHandler(connection));157});158159test('should handle errors during start', async () => {160// Setup delegate to fail on start161delegate.setCanStart(false);162163// Create server connection164const connection = instantiationService.createInstance(165McpServerConnection,166collection,167serverDefinition,168delegate,169serverDefinition.launch,170new NullLogger(),171false,172store.add(new McpTaskManager()),173);174store.add(connection);175176// Start the connection177const state = await connection.start({});178179assert.strictEqual(state.state, McpConnectionState.Kind.Error);180assert.ok(state.message);181});182183test('should handle transport errors', async () => {184// Create server connection185const connection = instantiationService.createInstance(186McpServerConnection,187collection,188serverDefinition,189delegate,190serverDefinition.launch,191new NullLogger(),192false,193store.add(new McpTaskManager()),194);195store.add(connection);196197// Start the connection198const startPromise = connection.start({});199200// Simulate error in transport201transport.setConnectionState({202state: McpConnectionState.Kind.Error,203message: 'Test error message'204});205206const state = await startPromise;207assert.strictEqual(state.state, McpConnectionState.Kind.Error);208assert.strictEqual(state.message, 'Test error message');209});210211test('should stop and set state to Stopped', async () => {212// Create server connection213const connection = instantiationService.createInstance(214McpServerConnection,215collection,216serverDefinition,217delegate,218serverDefinition.launch,219new NullLogger(),220false,221store.add(new McpTaskManager()),222);223store.add(connection);224225// Start the connection226const startPromise = connection.start({});227transport.setConnectionState({ state: McpConnectionState.Kind.Running });228await startPromise;229230// Stop the connection231const stopPromise = connection.stop();232await stopPromise;233234assert.strictEqual(connection.state.get().state, McpConnectionState.Kind.Stopped);235});236237test('should not restart if already starting', async () => {238// Create server connection239const connection = instantiationService.createInstance(240McpServerConnection,241collection,242serverDefinition,243delegate,244serverDefinition.launch,245new NullLogger(),246false,247store.add(new McpTaskManager()),248);249store.add(connection);250251// Start the connection252const startPromise1 = connection.start({});253254// Try to start again while starting255const startPromise2 = connection.start({});256257// Simulate successful connection258transport.setConnectionState({ state: McpConnectionState.Kind.Running });259260const state1 = await startPromise1;261const state2 = await startPromise2;262263// Both promises should resolve to the same state264assert.strictEqual(state1.state, McpConnectionState.Kind.Running);265assert.strictEqual(state2.state, McpConnectionState.Kind.Running);266267transport.simulateInitialized();268assert.ok(await waitForHandler(connection));269270connection.dispose();271});272273test('should clean up when disposed', async () => {274// Create server connection275const connection = instantiationService.createInstance(276McpServerConnection,277collection,278serverDefinition,279delegate,280serverDefinition.launch,281new NullLogger(),282false,283store.add(new McpTaskManager()),284);285286// Start the connection287const startPromise = connection.start({});288transport.setConnectionState({ state: McpConnectionState.Kind.Running });289await startPromise;290291// Dispose the connection292connection.dispose();293294assert.strictEqual(connection.state.get().state, McpConnectionState.Kind.Stopped);295});296297test('should log transport messages', async () => {298// Track logged messages299const loggedMessages: string[] = [];300301// Create server connection302const connection = instantiationService.createInstance(303McpServerConnection,304collection,305serverDefinition,306delegate,307serverDefinition.launch,308{309onDidChangeLogLevel: Event.None,310getLevel: () => LogLevel.Debug,311info: (message: string) => {312loggedMessages.push(message);313},314error: () => { },315dispose: () => { }316} as Partial<ILogger> as ILogger,317false,318store.add(new McpTaskManager()),319);320store.add(connection);321322// Start the connection323const startPromise = connection.start({});324325// Simulate log message from transport326transport.simulateLog('Test log message');327328// Set connection to running329transport.setConnectionState({ state: McpConnectionState.Kind.Running });330await startPromise;331332// Check that the message was logged333assert.ok(loggedMessages.some(msg => msg === 'Test log message'));334335connection.dispose();336await timeout(10);337});338339test('should correctly handle transitions to and from error state', async () => {340// Create server connection341const connection = instantiationService.createInstance(342McpServerConnection,343collection,344serverDefinition,345delegate,346serverDefinition.launch,347new NullLogger(),348false,349store.add(new McpTaskManager()),350);351store.add(connection);352353// Start the connection354const startPromise = connection.start({});355356// Transition to error state357const errorState: McpConnectionState = {358state: McpConnectionState.Kind.Error,359message: 'Temporary error'360};361transport.setConnectionState(errorState);362363let state = await startPromise;364assert.equal(state, errorState);365366367transport.setConnectionState({ state: McpConnectionState.Kind.Stopped });368369// Transition back to running state370const startPromise2 = connection.start({});371transport.setConnectionState({ state: McpConnectionState.Kind.Running });372state = await startPromise2;373assert.deepStrictEqual(state, { state: McpConnectionState.Kind.Running });374375connection.dispose();376await timeout(10);377});378379test('should handle multiple start/stop cycles', async () => {380// Create server connection381const connection = instantiationService.createInstance(382McpServerConnection,383collection,384serverDefinition,385delegate,386serverDefinition.launch,387new NullLogger(),388false,389store.add(new McpTaskManager()),390);391store.add(connection);392393// First cycle394let startPromise = connection.start({});395transport.setConnectionState({ state: McpConnectionState.Kind.Running });396await startPromise;397398await connection.stop();399assert.deepStrictEqual(connection.state.get(), { state: McpConnectionState.Kind.Stopped });400401// Second cycle402startPromise = connection.start({});403transport.setConnectionState({ state: McpConnectionState.Kind.Running });404await startPromise;405406assert.deepStrictEqual(connection.state.get(), { state: McpConnectionState.Kind.Running });407408await connection.stop();409410assert.deepStrictEqual(connection.state.get(), { state: McpConnectionState.Kind.Stopped });411412connection.dispose();413await timeout(10);414});415});416417418