Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.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 { upcast } from '../../../../../base/common/types.js';
8
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
9
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
10
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
11
import { ILoggerService } from '../../../../../platform/log/common/log.js';
12
import { IProductService } from '../../../../../platform/product/common/productService.js';
13
import { IStorageService } from '../../../../../platform/storage/common/storage.js';
14
import { TestLoggerService, TestProductService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';
15
import { IMcpHostDelegate } from '../../common/mcpRegistryTypes.js';
16
import { McpServerRequestHandler } from '../../common/mcpServerRequestHandler.js';
17
import { McpConnectionState } from '../../common/mcpTypes.js';
18
import { MCP } from '../../common/modelContextProtocol.js';
19
import { TestMcpMessageTransport } from './mcpRegistryTypes.js';
20
import { IOutputService } from '../../../../services/output/common/output.js';
21
import { Disposable } from '../../../../../base/common/lifecycle.js';
22
import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';
23
24
class TestMcpHostDelegate extends Disposable implements IMcpHostDelegate {
25
private readonly _transport: TestMcpMessageTransport;
26
27
priority = 0;
28
29
constructor() {
30
super();
31
this._transport = this._register(new TestMcpMessageTransport());
32
}
33
34
canStart(): boolean {
35
return true;
36
}
37
38
start(): TestMcpMessageTransport {
39
return this._transport;
40
}
41
42
getTransport(): TestMcpMessageTransport {
43
return this._transport;
44
}
45
46
waitForInitialProviderPromises(): Promise<void> {
47
return Promise.resolve();
48
}
49
}
50
51
suite('Workbench - MCP - ServerRequestHandler', () => {
52
const store = ensureNoDisposablesAreLeakedInTestSuite();
53
54
let instantiationService: TestInstantiationService;
55
let delegate: TestMcpHostDelegate;
56
let transport: TestMcpMessageTransport;
57
let handler: McpServerRequestHandler;
58
let cts: CancellationTokenSource;
59
60
setup(async () => {
61
delegate = store.add(new TestMcpHostDelegate());
62
transport = delegate.getTransport();
63
cts = store.add(new CancellationTokenSource());
64
65
// Setup test services
66
const services = new ServiceCollection(
67
[ILoggerService, store.add(new TestLoggerService())],
68
[IOutputService, upcast({ showChannel: () => { } })],
69
[IStorageService, store.add(new TestStorageService())],
70
[IProductService, TestProductService],
71
);
72
73
instantiationService = store.add(new TestInstantiationService(services));
74
75
transport.setConnectionState({ state: McpConnectionState.Kind.Running });
76
77
// Manually create the handler since we need the transport already set up
78
const logger = store.add((instantiationService.get(ILoggerService) as TestLoggerService)
79
.createLogger('mcpServerTest', { hidden: true, name: 'MCP Test' }));
80
81
// Start the handler creation
82
const handlerPromise = McpServerRequestHandler.create(instantiationService, { logger, launch: transport }, cts.token);
83
84
handler = await handlerPromise;
85
store.add(handler);
86
});
87
88
test('should send and receive JSON-RPC requests', async () => {
89
// Setup request
90
const requestPromise = handler.listResources();
91
92
// Get the sent message and verify it
93
const sentMessages = transport.getSentMessages();
94
assert.strictEqual(sentMessages.length, 3); // initialize + listResources
95
96
// Verify listResources request format
97
const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest;
98
assert.strictEqual(listResourcesRequest.method, 'resources/list');
99
assert.strictEqual(listResourcesRequest.jsonrpc, MCP.JSONRPC_VERSION);
100
assert.ok(typeof listResourcesRequest.id === 'number');
101
102
// Simulate server response with mock resources that match the expected Resource interface
103
transport.simulateReceiveMessage({
104
jsonrpc: MCP.JSONRPC_VERSION,
105
id: listResourcesRequest.id,
106
result: {
107
resources: [
108
{ uri: 'resource1', type: 'text/plain', name: 'Test Resource 1' },
109
{ uri: 'resource2', type: 'text/plain', name: 'Test Resource 2' }
110
]
111
}
112
});
113
114
// Verify the result
115
const resources = await requestPromise;
116
assert.strictEqual(resources.length, 2);
117
assert.strictEqual(resources[0].uri, 'resource1');
118
assert.strictEqual(resources[1].name, 'Test Resource 2');
119
});
120
121
test('should handle paginated requests', async () => {
122
// Setup request
123
const requestPromise = handler.listResources();
124
125
// Get the first request and respond with pagination
126
const sentMessages = transport.getSentMessages();
127
const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest;
128
129
// Send first page with nextCursor
130
transport.simulateReceiveMessage({
131
jsonrpc: MCP.JSONRPC_VERSION,
132
id: listResourcesRequest.id,
133
result: {
134
resources: [
135
{ uri: 'resource1', type: 'text/plain', name: 'Test Resource 1' }
136
],
137
nextCursor: 'page2'
138
}
139
});
140
141
// Clear the sent messages to only capture the next page request
142
transport.clearSentMessages();
143
144
// Wait a bit to allow the handler to process and send the next request
145
await new Promise(resolve => setTimeout(resolve, 0));
146
147
// Get the second request and verify cursor is included
148
const sentMessages2 = transport.getSentMessages();
149
assert.strictEqual(sentMessages2.length, 1);
150
151
const listResourcesRequest2 = sentMessages2[0] as MCP.JSONRPCRequest;
152
assert.strictEqual(listResourcesRequest2.method, 'resources/list');
153
assert.deepStrictEqual(listResourcesRequest2.params, { cursor: 'page2' });
154
155
// Send final page with no nextCursor
156
transport.simulateReceiveMessage({
157
jsonrpc: MCP.JSONRPC_VERSION,
158
id: listResourcesRequest2.id,
159
result: {
160
resources: [
161
{ uri: 'resource2', type: 'text/plain', name: 'Test Resource 2' }
162
]
163
}
164
});
165
166
// Verify the combined result
167
const resources = await requestPromise;
168
assert.strictEqual(resources.length, 2);
169
assert.strictEqual(resources[0].uri, 'resource1');
170
assert.strictEqual(resources[1].uri, 'resource2');
171
});
172
173
test('should handle error responses', async () => {
174
// Setup request
175
const requestPromise = handler.readResource({ uri: 'non-existent' });
176
177
// Get the sent message
178
const sentMessages = transport.getSentMessages();
179
const readResourceRequest = sentMessages[2] as MCP.JSONRPCRequest; // [0] is initialize
180
181
// Simulate error response
182
transport.simulateReceiveMessage({
183
jsonrpc: MCP.JSONRPC_VERSION,
184
id: readResourceRequest.id,
185
error: {
186
code: MCP.METHOD_NOT_FOUND,
187
message: 'Resource not found'
188
}
189
});
190
191
// Verify the error is thrown correctly
192
try {
193
await requestPromise;
194
assert.fail('Expected error was not thrown');
195
} catch (e: any) {
196
assert.strictEqual(e.message, 'MPC -32601: Resource not found');
197
assert.strictEqual(e.code, MCP.METHOD_NOT_FOUND);
198
}
199
});
200
201
test('should handle server requests', async () => {
202
// Simulate ping request from server
203
const pingRequest: MCP.JSONRPCRequest & MCP.PingRequest = {
204
jsonrpc: MCP.JSONRPC_VERSION,
205
id: 100,
206
method: 'ping'
207
};
208
209
transport.simulateReceiveMessage(pingRequest);
210
211
// The handler should have sent a response
212
const sentMessages = transport.getSentMessages();
213
const pingResponse = sentMessages.find(m =>
214
'id' in m && m.id === pingRequest.id && 'result' in m
215
) as MCP.JSONRPCResponse;
216
217
assert.ok(pingResponse, 'No ping response was sent');
218
assert.deepStrictEqual(pingResponse.result, {});
219
});
220
221
test('should handle roots list requests', async () => {
222
// Set roots
223
handler.roots = [
224
{ uri: 'file:///test/root1', name: 'Root 1' },
225
{ uri: 'file:///test/root2', name: 'Root 2' }
226
];
227
228
// Simulate roots/list request from server
229
const rootsRequest: MCP.JSONRPCRequest & MCP.ListRootsRequest = {
230
jsonrpc: MCP.JSONRPC_VERSION,
231
id: 101,
232
method: 'roots/list'
233
};
234
235
transport.simulateReceiveMessage(rootsRequest);
236
237
// The handler should have sent a response
238
const sentMessages = transport.getSentMessages();
239
const rootsResponse = sentMessages.find(m =>
240
'id' in m && m.id === rootsRequest.id && 'result' in m
241
) as MCP.JSONRPCResponse;
242
243
assert.ok(rootsResponse, 'No roots/list response was sent');
244
assert.strictEqual((rootsResponse.result as MCP.ListRootsResult).roots.length, 2);
245
assert.strictEqual((rootsResponse.result as MCP.ListRootsResult).roots[0].uri, 'file:///test/root1');
246
});
247
248
test('should handle server notifications', async () => {
249
let progressNotificationReceived = false;
250
store.add(handler.onDidReceiveProgressNotification(notification => {
251
progressNotificationReceived = true;
252
assert.strictEqual(notification.method, 'notifications/progress');
253
assert.strictEqual(notification.params.progressToken, 'token1');
254
assert.strictEqual(notification.params.progress, 50);
255
}));
256
257
// Simulate progress notification with correct format
258
const progressNotification: MCP.JSONRPCNotification & MCP.ProgressNotification = {
259
jsonrpc: MCP.JSONRPC_VERSION,
260
method: 'notifications/progress',
261
params: {
262
progressToken: 'token1',
263
progress: 50,
264
total: 100
265
}
266
};
267
268
transport.simulateReceiveMessage(progressNotification);
269
assert.strictEqual(progressNotificationReceived, true);
270
});
271
272
test('should handle cancellation', async () => {
273
// Setup a new cancellation token source for this specific test
274
const testCts = store.add(new CancellationTokenSource());
275
const requestPromise = handler.listResources(undefined, testCts.token);
276
277
// Get the request ID
278
const sentMessages = transport.getSentMessages();
279
const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest;
280
const requestId = listResourcesRequest.id;
281
282
// Cancel the request
283
testCts.cancel();
284
285
// Check that a cancellation notification was sent
286
const cancelNotification = transport.getSentMessages().find(m =>
287
!('id' in m) &&
288
'method' in m &&
289
m.method === 'notifications/cancelled' &&
290
'params' in m &&
291
m.params && m.params.requestId === requestId
292
);
293
294
assert.ok(cancelNotification, 'No cancellation notification was sent');
295
296
// Verify the promise was cancelled
297
try {
298
await requestPromise;
299
assert.fail('Promise should have been cancelled');
300
} catch (e) {
301
assert.strictEqual(e.name, 'Canceled');
302
}
303
});
304
305
test('should handle cancelled notification from server', async () => {
306
// Setup request
307
const requestPromise = handler.listResources();
308
309
// Get the request ID
310
const sentMessages = transport.getSentMessages();
311
const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest;
312
const requestId = listResourcesRequest.id;
313
314
// Simulate cancelled notification from server
315
const cancelledNotification: MCP.JSONRPCNotification & MCP.CancelledNotification = {
316
jsonrpc: MCP.JSONRPC_VERSION,
317
method: 'notifications/cancelled',
318
params: {
319
requestId
320
}
321
};
322
323
transport.simulateReceiveMessage(cancelledNotification);
324
325
// Verify the promise was cancelled
326
try {
327
await requestPromise;
328
assert.fail('Promise should have been cancelled');
329
} catch (e) {
330
assert.strictEqual(e.name, 'Canceled');
331
}
332
});
333
334
test('should dispose properly and cancel pending requests', async () => {
335
// Setup multiple requests
336
const request1 = handler.listResources();
337
const request2 = handler.listTools();
338
339
// Dispose the handler
340
handler.dispose();
341
342
// Verify all promises were cancelled
343
try {
344
await request1;
345
assert.fail('Promise 1 should have been cancelled');
346
} catch (e) {
347
assert.strictEqual(e.name, 'Canceled');
348
}
349
350
try {
351
await request2;
352
assert.fail('Promise 2 should have been cancelled');
353
} catch (e) {
354
assert.strictEqual(e.name, 'Canceled');
355
}
356
});
357
358
test('should handle connection error by cancelling requests', async () => {
359
// Setup request
360
const requestPromise = handler.listResources();
361
362
// Simulate connection error
363
transport.setConnectionState({
364
state: McpConnectionState.Kind.Error,
365
message: 'Connection lost'
366
});
367
368
// Verify the promise was cancelled
369
try {
370
await requestPromise;
371
assert.fail('Promise should have been cancelled');
372
} catch (e) {
373
assert.strictEqual(e.name, 'Canceled');
374
}
375
});
376
});
377
378