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