Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts
5263 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as assert from 'assert';
7
import { timeout } from '../../../../../base/common/async.js';
8
import { Disposable } from '../../../../../base/common/lifecycle.js';
9
import { autorun, observableValue } from '../../../../../base/common/observable.js';
10
import { upcast } from '../../../../../base/common/types.js';
11
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
12
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
13
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
14
import { ILogger, ILoggerService, LogLevel, NullLogger } from '../../../../../platform/log/common/log.js';
15
import { IProductService } from '../../../../../platform/product/common/productService.js';
16
import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js';
17
import { IOutputService } from '../../../../services/output/common/output.js';
18
import { TestLoggerService, TestProductService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';
19
import { IMcpHostDelegate, IMcpMessageTransport } from '../../common/mcpRegistryTypes.js';
20
import { McpServerConnection } from '../../common/mcpServerConnection.js';
21
import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust } from '../../common/mcpTypes.js';
22
import { TestMcpMessageTransport } from './mcpRegistryTypes.js';
23
import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js';
24
import { Event } from '../../../../../base/common/event.js';
25
import { McpTaskManager } from '../../common/mcpTaskManager.js';
26
27
class TestMcpHostDelegate extends Disposable implements IMcpHostDelegate {
28
private readonly _transport: TestMcpMessageTransport;
29
private _canStartValue = true;
30
31
priority = 0;
32
33
constructor() {
34
super();
35
this._transport = this._register(new TestMcpMessageTransport());
36
}
37
38
substituteVariables(serverDefinition: McpServerDefinition, launch: McpServerLaunch): Promise<McpServerLaunch> {
39
return Promise.resolve(launch);
40
}
41
42
canStart(): boolean {
43
return this._canStartValue;
44
}
45
46
start(): IMcpMessageTransport {
47
if (!this._canStartValue) {
48
throw new Error('Cannot start server');
49
}
50
return this._transport;
51
}
52
53
getTransport(): TestMcpMessageTransport {
54
return this._transport;
55
}
56
57
setCanStart(value: boolean): void {
58
this._canStartValue = value;
59
}
60
61
waitForInitialProviderPromises(): Promise<void> {
62
return Promise.resolve();
63
}
64
}
65
66
suite('Workbench - MCP - ServerConnection', () => {
67
const store = ensureNoDisposablesAreLeakedInTestSuite();
68
69
let instantiationService: TestInstantiationService;
70
let delegate: TestMcpHostDelegate;
71
let transport: TestMcpMessageTransport;
72
let collection: McpCollectionDefinition;
73
let serverDefinition: McpServerDefinition;
74
75
setup(() => {
76
delegate = store.add(new TestMcpHostDelegate());
77
transport = delegate.getTransport();
78
79
// Setup test services
80
const services = new ServiceCollection(
81
[ILoggerService, store.add(new TestLoggerService())],
82
[IOutputService, upcast({ showChannel: () => { } })],
83
[IStorageService, store.add(new TestStorageService())],
84
[IProductService, TestProductService],
85
);
86
87
instantiationService = store.add(new TestInstantiationService(services));
88
89
// Create test collection
90
collection = {
91
id: 'test-collection',
92
label: 'Test Collection',
93
remoteAuthority: null,
94
serverDefinitions: observableValue('serverDefs', []),
95
trustBehavior: McpServerTrust.Kind.Trusted,
96
scope: StorageScope.APPLICATION,
97
configTarget: ConfigurationTarget.USER,
98
};
99
100
// Create server definition
101
serverDefinition = {
102
id: 'test-server',
103
label: 'Test Server',
104
cacheNonce: 'a',
105
launch: {
106
type: McpServerTransportType.Stdio,
107
command: 'test-command',
108
args: [],
109
env: {},
110
envFile: undefined,
111
cwd: '/test'
112
}
113
};
114
});
115
116
function waitForHandler(cnx: McpServerConnection) {
117
const handler = cnx.handler.get();
118
if (handler) {
119
return Promise.resolve(handler);
120
}
121
122
return new Promise(resolve => {
123
const disposable = autorun(reader => {
124
const handler = cnx.handler.read(reader);
125
if (handler) {
126
disposable.dispose();
127
resolve(handler);
128
}
129
});
130
});
131
}
132
133
test('should start and set state to Running when transport succeeds', async () => {
134
// Create server connection
135
const connection = instantiationService.createInstance(
136
McpServerConnection,
137
collection,
138
serverDefinition,
139
delegate,
140
serverDefinition.launch,
141
new NullLogger(),
142
false,
143
store.add(new McpTaskManager()),
144
);
145
store.add(connection);
146
147
// Start the connection
148
const startPromise = connection.start({});
149
150
// Simulate successful connection
151
transport.setConnectionState({ state: McpConnectionState.Kind.Running });
152
153
const state = await startPromise;
154
assert.strictEqual(state.state, McpConnectionState.Kind.Running);
155
156
transport.simulateInitialized();
157
assert.ok(await waitForHandler(connection));
158
});
159
160
test('should handle errors during start', async () => {
161
// Setup delegate to fail on start
162
delegate.setCanStart(false);
163
164
// Create server connection
165
const connection = instantiationService.createInstance(
166
McpServerConnection,
167
collection,
168
serverDefinition,
169
delegate,
170
serverDefinition.launch,
171
new NullLogger(),
172
false,
173
store.add(new McpTaskManager()),
174
);
175
store.add(connection);
176
177
// Start the connection
178
const state = await connection.start({});
179
180
assert.strictEqual(state.state, McpConnectionState.Kind.Error);
181
assert.ok(state.message);
182
});
183
184
test('should handle transport errors', async () => {
185
// Create server connection
186
const connection = instantiationService.createInstance(
187
McpServerConnection,
188
collection,
189
serverDefinition,
190
delegate,
191
serverDefinition.launch,
192
new NullLogger(),
193
false,
194
store.add(new McpTaskManager()),
195
);
196
store.add(connection);
197
198
// Start the connection
199
const startPromise = connection.start({});
200
201
// Simulate error in transport
202
transport.setConnectionState({
203
state: McpConnectionState.Kind.Error,
204
message: 'Test error message'
205
});
206
207
const state = await startPromise;
208
assert.strictEqual(state.state, McpConnectionState.Kind.Error);
209
assert.strictEqual(state.message, 'Test error message');
210
});
211
212
test('should stop and set state to Stopped', async () => {
213
// Create server connection
214
const connection = instantiationService.createInstance(
215
McpServerConnection,
216
collection,
217
serverDefinition,
218
delegate,
219
serverDefinition.launch,
220
new NullLogger(),
221
false,
222
store.add(new McpTaskManager()),
223
);
224
store.add(connection);
225
226
// Start the connection
227
const startPromise = connection.start({});
228
transport.setConnectionState({ state: McpConnectionState.Kind.Running });
229
await startPromise;
230
231
// Stop the connection
232
const stopPromise = connection.stop();
233
await stopPromise;
234
235
assert.strictEqual(connection.state.get().state, McpConnectionState.Kind.Stopped);
236
});
237
238
test('should not restart if already starting', async () => {
239
// Create server connection
240
const connection = instantiationService.createInstance(
241
McpServerConnection,
242
collection,
243
serverDefinition,
244
delegate,
245
serverDefinition.launch,
246
new NullLogger(),
247
false,
248
store.add(new McpTaskManager()),
249
);
250
store.add(connection);
251
252
// Start the connection
253
const startPromise1 = connection.start({});
254
255
// Try to start again while starting
256
const startPromise2 = connection.start({});
257
258
// Simulate successful connection
259
transport.setConnectionState({ state: McpConnectionState.Kind.Running });
260
261
const state1 = await startPromise1;
262
const state2 = await startPromise2;
263
264
// Both promises should resolve to the same state
265
assert.strictEqual(state1.state, McpConnectionState.Kind.Running);
266
assert.strictEqual(state2.state, McpConnectionState.Kind.Running);
267
268
transport.simulateInitialized();
269
assert.ok(await waitForHandler(connection));
270
271
connection.dispose();
272
});
273
274
test('should clean up when disposed', async () => {
275
// Create server connection
276
const connection = instantiationService.createInstance(
277
McpServerConnection,
278
collection,
279
serverDefinition,
280
delegate,
281
serverDefinition.launch,
282
new NullLogger(),
283
false,
284
store.add(new McpTaskManager()),
285
);
286
287
// Start the connection
288
const startPromise = connection.start({});
289
transport.setConnectionState({ state: McpConnectionState.Kind.Running });
290
await startPromise;
291
292
// Dispose the connection
293
connection.dispose();
294
295
assert.strictEqual(connection.state.get().state, McpConnectionState.Kind.Stopped);
296
});
297
298
test('should log transport messages', async () => {
299
// Track logged messages
300
const loggedMessages: string[] = [];
301
302
// Create server connection
303
const connection = instantiationService.createInstance(
304
McpServerConnection,
305
collection,
306
serverDefinition,
307
delegate,
308
serverDefinition.launch,
309
{
310
onDidChangeLogLevel: Event.None,
311
getLevel: () => LogLevel.Debug,
312
info: (message: string) => {
313
loggedMessages.push(message);
314
},
315
error: () => { },
316
dispose: () => { }
317
} as Partial<ILogger> as ILogger,
318
false,
319
store.add(new McpTaskManager()),
320
);
321
store.add(connection);
322
323
// Start the connection
324
const startPromise = connection.start({});
325
326
// Simulate log message from transport
327
transport.simulateLog('Test log message');
328
329
// Set connection to running
330
transport.setConnectionState({ state: McpConnectionState.Kind.Running });
331
await startPromise;
332
333
// Check that the message was logged
334
assert.ok(loggedMessages.some(msg => msg === 'Test log message'));
335
336
connection.dispose();
337
await timeout(10);
338
});
339
340
test('should correctly handle transitions to and from error state', async () => {
341
// Create server connection
342
const connection = instantiationService.createInstance(
343
McpServerConnection,
344
collection,
345
serverDefinition,
346
delegate,
347
serverDefinition.launch,
348
new NullLogger(),
349
false,
350
store.add(new McpTaskManager()),
351
);
352
store.add(connection);
353
354
// Start the connection
355
const startPromise = connection.start({});
356
357
// Transition to error state
358
const errorState: McpConnectionState = {
359
state: McpConnectionState.Kind.Error,
360
message: 'Temporary error'
361
};
362
transport.setConnectionState(errorState);
363
364
let state = await startPromise;
365
assert.equal(state, errorState);
366
367
368
transport.setConnectionState({ state: McpConnectionState.Kind.Stopped });
369
370
// Transition back to running state
371
const startPromise2 = connection.start({});
372
transport.setConnectionState({ state: McpConnectionState.Kind.Running });
373
state = await startPromise2;
374
assert.deepStrictEqual(state, { state: McpConnectionState.Kind.Running });
375
376
connection.dispose();
377
await timeout(10);
378
});
379
380
test('should handle multiple start/stop cycles', async () => {
381
// Create server connection
382
const connection = instantiationService.createInstance(
383
McpServerConnection,
384
collection,
385
serverDefinition,
386
delegate,
387
serverDefinition.launch,
388
new NullLogger(),
389
false,
390
store.add(new McpTaskManager()),
391
);
392
store.add(connection);
393
394
// First cycle
395
let startPromise = connection.start({});
396
transport.setConnectionState({ state: McpConnectionState.Kind.Running });
397
await startPromise;
398
399
await connection.stop();
400
assert.deepStrictEqual(connection.state.get(), { state: McpConnectionState.Kind.Stopped });
401
402
// Second cycle
403
startPromise = connection.start({});
404
transport.setConnectionState({ state: McpConnectionState.Kind.Running });
405
await startPromise;
406
407
assert.deepStrictEqual(connection.state.get(), { state: McpConnectionState.Kind.Running });
408
409
await connection.stop();
410
411
assert.deepStrictEqual(connection.state.get(), { state: McpConnectionState.Kind.Stopped });
412
413
connection.dispose();
414
await timeout(10);
415
});
416
});
417
418