Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.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 * as sinon from 'sinon';
8
import { timeout } from '../../../../../base/common/async.js';
9
import { ISettableObservable, 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 { ConfigurationTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
13
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
14
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
15
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
16
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
17
import { ILogger, ILoggerService, ILogService, NullLogger, NullLogService } from '../../../../../platform/log/common/log.js';
18
import { mcpAccessConfig, McpAccessValue } from '../../../../../platform/mcp/common/mcpManagement.js';
19
import { IProductService } from '../../../../../platform/product/common/productService.js';
20
import { ISecretStorageService } from '../../../../../platform/secrets/common/secrets.js';
21
import { TestSecretStorageService } from '../../../../../platform/secrets/test/common/testSecretStorageService.js';
22
import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js';
23
import { IConfigurationResolverService } from '../../../../services/configurationResolver/common/configurationResolver.js';
24
import { ConfigurationResolverExpression } from '../../../../services/configurationResolver/common/configurationResolverExpression.js';
25
import { IOutputService } from '../../../../services/output/common/output.js';
26
import { TestLoggerService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';
27
import { McpRegistry } from '../../common/mcpRegistry.js';
28
import { IMcpHostDelegate, IMcpMessageTransport } from '../../common/mcpRegistryTypes.js';
29
import { McpServerConnection } from '../../common/mcpServerConnection.js';
30
import { LazyCollectionState, McpCollectionDefinition, McpServerDefinition, McpServerTransportStdio, McpServerTransportType, McpServerTrust, McpStartServerInteraction } from '../../common/mcpTypes.js';
31
import { TestMcpMessageTransport } from './mcpRegistryTypes.js';
32
33
class TestConfigurationResolverService implements Partial<IConfigurationResolverService> {
34
declare readonly _serviceBrand: undefined;
35
36
private interactiveCounter = 0;
37
38
// Used to simulate stored/resolved variables
39
private readonly resolvedVariables = new Map<string, string>();
40
41
constructor() {
42
// Add some test variables
43
this.resolvedVariables.set('workspaceFolder', '/test/workspace');
44
this.resolvedVariables.set('fileBasename', 'test.txt');
45
}
46
47
resolveAsync(folder: any, value: any): Promise<any> {
48
const parsed = ConfigurationResolverExpression.parse(value);
49
for (const variable of parsed.unresolved()) {
50
const resolved = this.resolvedVariables.get(variable.inner);
51
if (resolved) {
52
parsed.resolve(variable, resolved);
53
}
54
}
55
56
return Promise.resolve(parsed.toObject());
57
}
58
59
resolveWithInteraction(folder: any, config: any, section?: string, variables?: Record<string, string>, target?: ConfigurationTarget): Promise<Map<string, string> | undefined> {
60
const parsed = ConfigurationResolverExpression.parse(config);
61
// For testing, we simulate interaction by returning a map with some variables
62
const result = new Map<string, string>();
63
result.set('input:testInteractive', `interactiveValue${this.interactiveCounter++}`);
64
result.set('command:testCommand', `commandOutput${this.interactiveCounter++}}`);
65
66
// If variables are provided, include those too
67
for (const [k, v] of result.entries()) {
68
parsed.resolve({ id: '${' + k + '}' } as any, v);
69
}
70
71
return Promise.resolve(result);
72
}
73
}
74
75
class TestMcpHostDelegate implements IMcpHostDelegate {
76
priority = 0;
77
78
canStart(): boolean {
79
return true;
80
}
81
82
start(): IMcpMessageTransport {
83
return new TestMcpMessageTransport();
84
}
85
86
waitForInitialProviderPromises(): Promise<void> {
87
return Promise.resolve();
88
}
89
}
90
91
class TestDialogService implements Partial<IDialogService> {
92
declare readonly _serviceBrand: undefined;
93
94
private _promptResult: boolean | undefined;
95
private _promptSpy: sinon.SinonStub;
96
97
constructor() {
98
this._promptSpy = sinon.stub();
99
this._promptSpy.callsFake(() => {
100
return Promise.resolve({ result: this._promptResult });
101
});
102
}
103
104
setPromptResult(result: boolean | undefined): void {
105
this._promptResult = result;
106
}
107
108
get promptSpy(): sinon.SinonStub {
109
return this._promptSpy;
110
}
111
112
prompt(options: any): Promise<any> {
113
return this._promptSpy(options);
114
}
115
}
116
117
class TestMcpRegistry extends McpRegistry {
118
public nextDefinitionIdsToTrust: string[] | undefined;
119
120
protected override _promptForTrustOpenDialog(): Promise<string[] | undefined> {
121
return Promise.resolve(this.nextDefinitionIdsToTrust);
122
}
123
}
124
125
suite('Workbench - MCP - Registry', () => {
126
const store = ensureNoDisposablesAreLeakedInTestSuite();
127
128
let registry: TestMcpRegistry;
129
let testStorageService: TestStorageService;
130
let testConfigResolverService: TestConfigurationResolverService;
131
let testDialogService: TestDialogService;
132
let testCollection: McpCollectionDefinition & { serverDefinitions: ISettableObservable<McpServerDefinition[]> };
133
let baseDefinition: McpServerDefinition;
134
let configurationService: TestConfigurationService;
135
let logger: ILogger;
136
let trustNonceBearer: { trustedAtNonce: string | undefined };
137
138
setup(() => {
139
testConfigResolverService = new TestConfigurationResolverService();
140
testStorageService = store.add(new TestStorageService());
141
testDialogService = new TestDialogService();
142
configurationService = new TestConfigurationService({ [mcpAccessConfig]: McpAccessValue.All });
143
trustNonceBearer = { trustedAtNonce: undefined };
144
145
const services = new ServiceCollection(
146
[IConfigurationService, configurationService],
147
[IConfigurationResolverService, testConfigResolverService],
148
[IStorageService, testStorageService],
149
[ISecretStorageService, new TestSecretStorageService()],
150
[ILoggerService, store.add(new TestLoggerService())],
151
[ILogService, store.add(new NullLogService())],
152
[IOutputService, upcast({ showChannel: () => { } })],
153
[IDialogService, testDialogService],
154
[IProductService, {}],
155
);
156
157
logger = new NullLogger();
158
159
const instaService = store.add(new TestInstantiationService(services));
160
registry = store.add(instaService.createInstance(TestMcpRegistry));
161
162
// Create test collection that can be reused
163
testCollection = {
164
id: 'test-collection',
165
label: 'Test Collection',
166
remoteAuthority: null,
167
serverDefinitions: observableValue('serverDefs', []),
168
trustBehavior: McpServerTrust.Kind.Trusted,
169
scope: StorageScope.APPLICATION,
170
configTarget: ConfigurationTarget.USER,
171
};
172
173
// Create base definition that can be reused
174
baseDefinition = {
175
id: 'test-server',
176
label: 'Test Server',
177
cacheNonce: 'a',
178
launch: {
179
type: McpServerTransportType.Stdio,
180
command: 'test-command',
181
args: [],
182
env: {},
183
envFile: undefined,
184
cwd: '/test',
185
}
186
};
187
});
188
189
test('registerCollection adds collection to registry', () => {
190
const disposable = registry.registerCollection(testCollection);
191
store.add(disposable);
192
193
assert.strictEqual(registry.collections.get().length, 1);
194
assert.strictEqual(registry.collections.get()[0], testCollection);
195
196
disposable.dispose();
197
assert.strictEqual(registry.collections.get().length, 0);
198
});
199
200
test('collections are not visible when not enabled', () => {
201
const disposable = registry.registerCollection(testCollection);
202
store.add(disposable);
203
204
assert.strictEqual(registry.collections.get().length, 1);
205
206
configurationService.setUserConfiguration(mcpAccessConfig, McpAccessValue.None);
207
configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any);
208
209
assert.strictEqual(registry.collections.get().length, 0);
210
211
configurationService.setUserConfiguration(mcpAccessConfig, McpAccessValue.All);
212
configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any);
213
});
214
215
test('registerDelegate adds delegate to registry', () => {
216
const delegate = new TestMcpHostDelegate();
217
const disposable = registry.registerDelegate(delegate);
218
store.add(disposable);
219
220
assert.strictEqual(registry.delegates.get().length, 1);
221
assert.strictEqual(registry.delegates.get()[0], delegate);
222
223
disposable.dispose();
224
assert.strictEqual(registry.delegates.get().length, 0);
225
});
226
227
test('resolveConnection creates connection with resolved variables and memorizes them until cleared', async () => {
228
const definition: McpServerDefinition = {
229
...baseDefinition,
230
launch: {
231
type: McpServerTransportType.Stdio,
232
command: '${workspaceFolder}/cmd',
233
args: ['--file', '${fileBasename}'],
234
env: {
235
PATH: '${input:testInteractive}'
236
},
237
envFile: undefined,
238
cwd: '/test',
239
},
240
variableReplacement: {
241
section: 'mcp',
242
target: ConfigurationTarget.WORKSPACE,
243
}
244
};
245
246
const delegate = new TestMcpHostDelegate();
247
store.add(registry.registerDelegate(delegate));
248
testCollection.serverDefinitions.set([definition], undefined);
249
store.add(registry.registerCollection(testCollection));
250
251
const connection = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger, trustNonceBearer }) as McpServerConnection;
252
253
assert.ok(connection);
254
assert.strictEqual(connection.definition, definition);
255
assert.strictEqual((connection.launchDefinition as any).command, '/test/workspace/cmd');
256
assert.strictEqual((connection.launchDefinition as any).env.PATH, 'interactiveValue0');
257
connection.dispose();
258
259
const connection2 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger, trustNonceBearer }) as McpServerConnection;
260
261
assert.ok(connection2);
262
assert.strictEqual((connection2.launchDefinition as any).env.PATH, 'interactiveValue0');
263
connection2.dispose();
264
265
registry.clearSavedInputs(StorageScope.WORKSPACE);
266
267
const connection3 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger, trustNonceBearer }) as McpServerConnection;
268
269
assert.ok(connection3);
270
assert.strictEqual((connection3.launchDefinition as any).env.PATH, 'interactiveValue4');
271
connection3.dispose();
272
});
273
274
test('resolveConnection uses user-provided launch configuration', async () => {
275
// Create a collection with custom launch resolver
276
const customCollection: McpCollectionDefinition = {
277
...testCollection,
278
resolveServerLanch: async (def) => {
279
return {
280
...(def.launch as McpServerTransportStdio),
281
env: { CUSTOM_ENV: 'value' },
282
};
283
}
284
};
285
286
// Create a definition with variable replacement
287
const definition: McpServerDefinition = {
288
...baseDefinition,
289
variableReplacement: {
290
section: 'mcp',
291
target: ConfigurationTarget.WORKSPACE,
292
}
293
};
294
295
const delegate = new TestMcpHostDelegate();
296
store.add(registry.registerDelegate(delegate));
297
testCollection.serverDefinitions.set([definition], undefined);
298
store.add(registry.registerCollection(customCollection));
299
300
// Resolve connection should use the custom launch configuration
301
const connection = await registry.resolveConnection({
302
collectionRef: customCollection,
303
definitionRef: definition,
304
logger,
305
trustNonceBearer,
306
}) as McpServerConnection;
307
308
assert.ok(connection);
309
310
// Verify the launch configuration passed to _replaceVariablesInLaunch was the custom one
311
assert.deepStrictEqual((connection.launchDefinition as McpServerTransportStdio).env, { CUSTOM_ENV: 'value' });
312
313
connection.dispose();
314
});
315
316
suite('Lazy Collections', () => {
317
let lazyCollection: McpCollectionDefinition;
318
let normalCollection: McpCollectionDefinition;
319
let removedCalled: boolean;
320
321
setup(() => {
322
removedCalled = false;
323
lazyCollection = {
324
...testCollection,
325
id: 'lazy-collection',
326
lazy: {
327
isCached: false,
328
load: () => Promise.resolve(),
329
removed: () => { removedCalled = true; }
330
}
331
};
332
normalCollection = {
333
...testCollection,
334
id: 'lazy-collection',
335
serverDefinitions: observableValue('serverDefs', [baseDefinition])
336
};
337
});
338
339
test('registers lazy collection', () => {
340
const disposable = registry.registerCollection(lazyCollection);
341
store.add(disposable);
342
343
assert.strictEqual(registry.collections.get().length, 1);
344
assert.strictEqual(registry.collections.get()[0], lazyCollection);
345
assert.strictEqual(registry.lazyCollectionState.get().state, LazyCollectionState.HasUnknown);
346
});
347
348
test('lazy collection is replaced by normal collection', () => {
349
store.add(registry.registerCollection(lazyCollection));
350
store.add(registry.registerCollection(normalCollection));
351
352
const collections = registry.collections.get();
353
assert.strictEqual(collections.length, 1);
354
assert.strictEqual(collections[0], normalCollection);
355
assert.strictEqual(collections[0].lazy, undefined);
356
assert.strictEqual(registry.lazyCollectionState.get().state, LazyCollectionState.AllKnown);
357
});
358
359
test('lazyCollectionState updates correctly during loading', async () => {
360
lazyCollection = {
361
...lazyCollection,
362
lazy: {
363
...lazyCollection.lazy!,
364
load: async () => {
365
await timeout(0);
366
store.add(registry.registerCollection(normalCollection));
367
return Promise.resolve();
368
}
369
}
370
};
371
372
store.add(registry.registerCollection(lazyCollection));
373
assert.strictEqual(registry.lazyCollectionState.get().state, LazyCollectionState.HasUnknown);
374
375
const loadingPromise = registry.discoverCollections();
376
assert.strictEqual(registry.lazyCollectionState.get().state, LazyCollectionState.LoadingUnknown);
377
378
await loadingPromise;
379
380
// The collection wasn't replaced, so it should be removed
381
assert.strictEqual(registry.collections.get().length, 1);
382
assert.strictEqual(registry.lazyCollectionState.get().state, LazyCollectionState.AllKnown);
383
assert.strictEqual(removedCalled, false);
384
});
385
386
test('removed callback is called when lazy collection is not replaced', async () => {
387
store.add(registry.registerCollection(lazyCollection));
388
await registry.discoverCollections();
389
390
assert.strictEqual(removedCalled, true);
391
});
392
393
test('cached lazy collections are tracked correctly', () => {
394
lazyCollection.lazy!.isCached = true;
395
store.add(registry.registerCollection(lazyCollection));
396
397
assert.strictEqual(registry.lazyCollectionState.get().state, LazyCollectionState.AllKnown);
398
399
// Adding an uncached lazy collection changes the state
400
const uncachedLazy = {
401
...lazyCollection,
402
id: 'uncached-lazy',
403
lazy: {
404
...lazyCollection.lazy!,
405
isCached: false
406
}
407
};
408
store.add(registry.registerCollection(uncachedLazy));
409
410
assert.strictEqual(registry.lazyCollectionState.get().state, LazyCollectionState.HasUnknown);
411
});
412
});
413
414
suite('Trust Flow', () => {
415
/**
416
* Helper to create a test MCP collection with a specific trust behavior
417
*/
418
function createTestCollection(trustBehavior: McpServerTrust.Kind.Trusted | McpServerTrust.Kind.TrustedOnNonce, id = 'test-collection'): McpCollectionDefinition & { serverDefinitions: ISettableObservable<McpServerDefinition[]> } {
419
return {
420
id,
421
label: 'Test Collection',
422
remoteAuthority: null,
423
serverDefinitions: observableValue('serverDefs', []),
424
trustBehavior,
425
scope: StorageScope.APPLICATION,
426
configTarget: ConfigurationTarget.USER,
427
};
428
}
429
430
/**
431
* Helper to create a test server definition with a specific cache nonce
432
*/
433
function createTestDefinition(id = 'test-server', cacheNonce = 'nonce-a'): McpServerDefinition {
434
return {
435
id,
436
label: 'Test Server',
437
cacheNonce,
438
launch: {
439
type: McpServerTransportType.Stdio,
440
command: 'test-command',
441
args: [],
442
env: {},
443
envFile: undefined,
444
cwd: '/test',
445
}
446
};
447
}
448
449
/**
450
* Helper to set up a basic registry with delegate and collection
451
*/
452
function setupRegistry(trustBehavior: McpServerTrust.Kind.Trusted | McpServerTrust.Kind.TrustedOnNonce = McpServerTrust.Kind.TrustedOnNonce, cacheNonce = 'nonce-a') {
453
const delegate = new TestMcpHostDelegate();
454
store.add(registry.registerDelegate(delegate));
455
456
const collection = createTestCollection(trustBehavior);
457
const definition = createTestDefinition('test-server', cacheNonce);
458
collection.serverDefinitions.set([definition], undefined);
459
store.add(registry.registerCollection(collection));
460
461
return { collection, definition, delegate };
462
}
463
464
test('trusted collection allows connection without prompting', async () => {
465
const { collection, definition } = setupRegistry(McpServerTrust.Kind.Trusted);
466
467
const connection = await registry.resolveConnection({
468
collectionRef: collection,
469
definitionRef: definition,
470
logger,
471
trustNonceBearer,
472
});
473
474
assert.ok(connection, 'Connection should be created for trusted collection');
475
assert.strictEqual(registry.nextDefinitionIdsToTrust, undefined, 'Trust dialog should not have been called');
476
connection!.dispose();
477
});
478
479
test('nonce-based trust allows connection when nonce matches', async () => {
480
const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-a');
481
trustNonceBearer.trustedAtNonce = 'nonce-a';
482
483
const connection = await registry.resolveConnection({
484
collectionRef: collection,
485
definitionRef: definition,
486
logger,
487
trustNonceBearer,
488
});
489
490
assert.ok(connection, 'Connection should be created when nonce matches');
491
assert.strictEqual(registry.nextDefinitionIdsToTrust, undefined, 'Trust dialog should not have been called');
492
connection!.dispose();
493
});
494
495
test('nonce-based trust prompts when nonce changes', async () => {
496
const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');
497
trustNonceBearer.trustedAtNonce = 'nonce-a'; // Different nonce
498
registry.nextDefinitionIdsToTrust = [definition.id]; // User trusts the server
499
500
const connection = await registry.resolveConnection({
501
collectionRef: collection,
502
definitionRef: definition,
503
logger,
504
trustNonceBearer,
505
});
506
507
assert.ok(connection, 'Connection should be created when user trusts');
508
assert.strictEqual(trustNonceBearer.trustedAtNonce, 'nonce-b', 'Nonce should be updated');
509
connection!.dispose();
510
});
511
512
test('nonce-based trust denies connection when user rejects', async () => {
513
const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');
514
trustNonceBearer.trustedAtNonce = 'nonce-a'; // Different nonce
515
registry.nextDefinitionIdsToTrust = []; // User does not trust the server
516
517
const connection = await registry.resolveConnection({
518
collectionRef: collection,
519
definitionRef: definition,
520
logger,
521
trustNonceBearer,
522
});
523
524
assert.strictEqual(connection, undefined, 'Connection should not be created when user rejects');
525
assert.strictEqual(trustNonceBearer.trustedAtNonce, '__vscode_not_trusted', 'Should mark as explicitly not trusted');
526
});
527
528
test('autoTrustChanges bypasses prompt when nonce changes', async () => {
529
const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');
530
trustNonceBearer.trustedAtNonce = 'nonce-a'; // Different nonce
531
532
const connection = await registry.resolveConnection({
533
collectionRef: collection,
534
definitionRef: definition,
535
logger,
536
trustNonceBearer,
537
autoTrustChanges: true,
538
});
539
540
assert.ok(connection, 'Connection should be created with autoTrustChanges');
541
assert.strictEqual(trustNonceBearer.trustedAtNonce, 'nonce-b', 'Nonce should be updated');
542
assert.strictEqual(registry.nextDefinitionIdsToTrust, undefined, 'Trust dialog should not have been called');
543
connection!.dispose();
544
});
545
546
test('promptType "never" skips prompt and fails silently', async () => {
547
const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');
548
trustNonceBearer.trustedAtNonce = 'nonce-a'; // Different nonce
549
550
const connection = await registry.resolveConnection({
551
collectionRef: collection,
552
definitionRef: definition,
553
logger,
554
trustNonceBearer,
555
promptType: 'never',
556
});
557
558
assert.strictEqual(connection, undefined, 'Connection should not be created with promptType "never"');
559
assert.strictEqual(registry.nextDefinitionIdsToTrust, undefined, 'Trust dialog should not have been called');
560
});
561
562
test('promptType "only-new" skips previously untrusted servers', async () => {
563
const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');
564
trustNonceBearer.trustedAtNonce = '__vscode_not_trusted'; // Previously explicitly denied
565
566
const connection = await registry.resolveConnection({
567
collectionRef: collection,
568
definitionRef: definition,
569
logger,
570
trustNonceBearer,
571
promptType: 'only-new',
572
});
573
574
assert.strictEqual(connection, undefined, 'Connection should not be created for previously untrusted server');
575
assert.strictEqual(registry.nextDefinitionIdsToTrust, undefined, 'Trust dialog should not have been called');
576
});
577
578
test('promptType "all-untrusted" prompts for previously untrusted servers', async () => {
579
const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');
580
trustNonceBearer.trustedAtNonce = '__vscode_not_trusted'; // Previously explicitly denied
581
registry.nextDefinitionIdsToTrust = [definition.id]; // User now trusts the server
582
583
const connection = await registry.resolveConnection({
584
collectionRef: collection,
585
definitionRef: definition,
586
logger,
587
trustNonceBearer,
588
promptType: 'all-untrusted',
589
});
590
591
assert.ok(connection, 'Connection should be created when user trusts previously untrusted server');
592
assert.strictEqual(trustNonceBearer.trustedAtNonce, 'nonce-b', 'Nonce should be updated');
593
connection!.dispose();
594
});
595
596
test('concurrent resolveConnection calls with same interaction are grouped', async () => {
597
const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');
598
trustNonceBearer.trustedAtNonce = 'nonce-a'; // Different nonce
599
600
// Create a second definition that also needs trust
601
const definition2 = createTestDefinition('test-server-2', 'nonce-c');
602
collection.serverDefinitions.set([definition, definition2], undefined);
603
604
// Create shared interaction
605
const interaction = new McpStartServerInteraction();
606
607
// Manually set participants as mentioned in the requirements
608
interaction.participants.set(definition.id, { s: 'unknown' });
609
interaction.participants.set(definition2.id, { s: 'unknown' });
610
611
const trustNonceBearer2 = { trustedAtNonce: 'nonce-b' }; // Different nonce for second server
612
613
// Trust both servers
614
registry.nextDefinitionIdsToTrust = [definition.id, definition2.id];
615
616
// Start both connections concurrently with the same interaction
617
const [connection1, connection2] = await Promise.all([
618
registry.resolveConnection({
619
collectionRef: collection,
620
definitionRef: definition,
621
logger,
622
trustNonceBearer,
623
interaction,
624
}),
625
registry.resolveConnection({
626
collectionRef: collection,
627
definitionRef: definition2,
628
logger,
629
trustNonceBearer: trustNonceBearer2,
630
interaction,
631
})
632
]);
633
634
assert.ok(connection1, 'First connection should be created');
635
assert.ok(connection2, 'Second connection should be created');
636
assert.strictEqual(trustNonceBearer.trustedAtNonce, 'nonce-b', 'First nonce should be updated');
637
assert.strictEqual(trustNonceBearer2.trustedAtNonce, 'nonce-c', 'Second nonce should be updated');
638
639
connection1!.dispose();
640
connection2!.dispose();
641
});
642
643
test('user cancelling trust dialog returns undefined for all pending connections', async () => {
644
const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');
645
trustNonceBearer.trustedAtNonce = 'nonce-a'; // Different nonce
646
647
// Create a second definition that also needs trust
648
const definition2 = createTestDefinition('test-server-2', 'nonce-c');
649
collection.serverDefinitions.set([definition, definition2], undefined);
650
651
// Create shared interaction
652
const interaction = new McpStartServerInteraction();
653
654
// Manually set participants as mentioned in the requirements
655
interaction.participants.set(definition.id, { s: 'unknown' });
656
interaction.participants.set(definition2.id, { s: 'unknown' });
657
658
const trustNonceBearer2 = { trustedAtNonce: 'nonce-b' }; // Different nonce for second server
659
660
// User cancels the dialog
661
registry.nextDefinitionIdsToTrust = undefined;
662
663
// Start both connections concurrently with the same interaction
664
const [connection1, connection2] = await Promise.all([
665
registry.resolveConnection({
666
collectionRef: collection,
667
definitionRef: definition,
668
logger,
669
trustNonceBearer,
670
interaction,
671
}),
672
registry.resolveConnection({
673
collectionRef: collection,
674
definitionRef: definition2,
675
logger,
676
trustNonceBearer: trustNonceBearer2,
677
interaction,
678
})
679
]);
680
681
assert.strictEqual(connection1, undefined, 'First connection should not be created when user cancels');
682
assert.strictEqual(connection2, undefined, 'Second connection should not be created when user cancels');
683
});
684
685
test('partial trust selection in grouped interaction', async () => {
686
const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');
687
trustNonceBearer.trustedAtNonce = 'nonce-a'; // Different nonce
688
689
// Create a second definition that also needs trust
690
const definition2 = createTestDefinition('test-server-2', 'nonce-c');
691
collection.serverDefinitions.set([definition, definition2], undefined);
692
693
// Create shared interaction
694
const interaction = new McpStartServerInteraction();
695
696
// Manually set participants as mentioned in the requirements
697
interaction.participants.set(definition.id, { s: 'unknown' });
698
interaction.participants.set(definition2.id, { s: 'unknown' });
699
700
const trustNonceBearer2 = { trustedAtNonce: 'nonce-b' }; // Different nonce for second server
701
702
// User trusts only the first server
703
registry.nextDefinitionIdsToTrust = [definition.id];
704
705
// Start both connections concurrently with the same interaction
706
const [connection1, connection2] = await Promise.all([
707
registry.resolveConnection({
708
collectionRef: collection,
709
definitionRef: definition,
710
logger,
711
trustNonceBearer,
712
interaction,
713
}),
714
registry.resolveConnection({
715
collectionRef: collection,
716
definitionRef: definition2,
717
logger,
718
trustNonceBearer: trustNonceBearer2,
719
interaction,
720
})
721
]);
722
723
assert.ok(connection1, 'First connection should be created when trusted');
724
assert.strictEqual(connection2, undefined, 'Second connection should not be created when not trusted');
725
assert.strictEqual(trustNonceBearer.trustedAtNonce, 'nonce-b', 'First nonce should be updated');
726
assert.strictEqual(trustNonceBearer2.trustedAtNonce, '__vscode_not_trusted', 'Second nonce should be marked as not trusted');
727
728
connection1!.dispose();
729
});
730
});
731
732
});
733
734