Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/mcp/test/common/mcpManagementService.test.ts
5245 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 assert from 'assert';
7
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
8
import { AbstractCommonMcpManagementService } from '../../common/mcpManagementService.js';
9
import { IGalleryMcpServer, IGalleryMcpServerConfiguration, IInstallableMcpServer, ILocalMcpServer, InstallOptions, RegistryType, TransportType, UninstallOptions } from '../../common/mcpManagement.js';
10
import { McpServerType, McpServerVariableType, IMcpServerVariable } from '../../common/mcpPlatformTypes.js';
11
import { IMarkdownString } from '../../../../base/common/htmlContent.js';
12
import { Event } from '../../../../base/common/event.js';
13
import { URI } from '../../../../base/common/uri.js';
14
import { NullLogService } from '../../../log/common/log.js';
15
16
class TestMcpManagementService extends AbstractCommonMcpManagementService {
17
18
override onInstallMcpServer = Event.None;
19
override onDidInstallMcpServers = Event.None;
20
override onDidUpdateMcpServers = Event.None;
21
override onUninstallMcpServer = Event.None;
22
override onDidUninstallMcpServer = Event.None;
23
24
override getInstalled(mcpResource?: URI): Promise<ILocalMcpServer[]> {
25
throw new Error('Method not implemented.');
26
}
27
override install(server: IInstallableMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {
28
throw new Error('Method not implemented.');
29
}
30
override installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {
31
throw new Error('Method not implemented.');
32
}
33
override updateMetadata(local: ILocalMcpServer, server: IGalleryMcpServer, profileLocation?: URI): Promise<ILocalMcpServer> {
34
throw new Error('Method not implemented.');
35
}
36
override uninstall(server: ILocalMcpServer, options?: UninstallOptions): Promise<void> {
37
throw new Error('Method not implemented.');
38
}
39
40
override canInstall(server: IGalleryMcpServer | IInstallableMcpServer): true | IMarkdownString {
41
throw new Error('Not supported');
42
}
43
}
44
45
suite('McpManagementService - getMcpServerConfigurationFromManifest', () => {
46
let service: TestMcpManagementService;
47
48
setup(() => {
49
service = new TestMcpManagementService(new NullLogService());
50
});
51
52
teardown(() => {
53
service.dispose();
54
});
55
56
ensureNoDisposablesAreLeakedInTestSuite();
57
58
suite('NPM Package Tests', () => {
59
test('basic NPM package configuration', () => {
60
const manifest: IGalleryMcpServerConfiguration = {
61
packages: [{
62
registryType: RegistryType.NODE,
63
identifier: '@modelcontextprotocol/server-brave-search',
64
transport: { type: TransportType.STDIO },
65
version: '1.0.2',
66
environmentVariables: [{
67
name: 'BRAVE_API_KEY',
68
value: 'test-key'
69
}]
70
}]
71
};
72
73
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
74
75
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);
76
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
77
assert.strictEqual(result.mcpServerConfiguration.config.command, 'npx');
78
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['@modelcontextprotocol/[email protected]']);
79
assert.deepStrictEqual(result.mcpServerConfiguration.config.env, { 'BRAVE_API_KEY': 'test-key' });
80
}
81
assert.strictEqual(result.mcpServerConfiguration.inputs, undefined);
82
});
83
84
test('NPM package with custom registry URL', () => {
85
const manifest: IGalleryMcpServerConfiguration = {
86
packages: [{
87
registryType: RegistryType.NODE,
88
registryBaseUrl: 'https://custom-registry.example.com',
89
identifier: '@company/internal-package',
90
transport: { type: TransportType.STDIO },
91
version: '2.1.0'
92
}]
93
};
94
95
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
96
97
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);
98
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
99
assert.strictEqual(result.mcpServerConfiguration.config.command, 'npx');
100
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [
101
'--registry', 'https://custom-registry.example.com',
102
'@company/[email protected]'
103
]);
104
}
105
});
106
107
test('NPM package without version', () => {
108
const manifest: IGalleryMcpServerConfiguration = {
109
packages: [{
110
registryType: RegistryType.NODE,
111
identifier: '@modelcontextprotocol/everything',
112
version: '',
113
transport: { type: TransportType.STDIO }
114
}]
115
};
116
117
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
118
119
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);
120
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
121
assert.strictEqual(result.mcpServerConfiguration.config.command, 'npx');
122
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['@modelcontextprotocol/everything']);
123
}
124
});
125
126
test('NPM package with environment variables containing variables', () => {
127
const manifest: IGalleryMcpServerConfiguration = {
128
packages: [{
129
registryType: RegistryType.NODE,
130
transport: { type: TransportType.STDIO },
131
identifier: 'test-server',
132
version: '1.0.0',
133
environmentVariables: [{
134
name: 'API_KEY',
135
value: 'key-{api_token}',
136
variables: {
137
api_token: {
138
description: 'Your API token',
139
isSecret: true,
140
isRequired: true
141
}
142
}
143
}]
144
}]
145
};
146
147
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
148
149
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);
150
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
151
assert.deepStrictEqual(result.mcpServerConfiguration.config.env, { 'API_KEY': 'key-${input:api_token}' });
152
}
153
assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);
154
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].id, 'api_token');
155
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].type, McpServerVariableType.PROMPT);
156
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].description, 'Your API token');
157
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].password, true);
158
});
159
160
test('environment variable with empty value should create input variable (GitHub issue #266106)', () => {
161
const manifest: IGalleryMcpServerConfiguration = {
162
packages: [{
163
registryType: RegistryType.NODE,
164
transport: { type: TransportType.STDIO },
165
identifier: '@modelcontextprotocol/server-brave-search',
166
version: '1.0.2',
167
environmentVariables: [{
168
name: 'BRAVE_API_KEY',
169
value: '', // Empty value should create input variable
170
description: 'Brave Search API Key',
171
isRequired: true,
172
isSecret: true
173
}]
174
}]
175
};
176
177
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
178
179
// BUG: Currently this creates env with empty string instead of input variable
180
// Should create an input variable since no meaningful value is provided
181
assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);
182
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].id, 'BRAVE_API_KEY');
183
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].description, 'Brave Search API Key');
184
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].password, true);
185
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].type, McpServerVariableType.PROMPT);
186
187
// Environment should use input variable interpolation
188
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
189
assert.deepStrictEqual(result.mcpServerConfiguration.config.env, { 'BRAVE_API_KEY': '${input:BRAVE_API_KEY}' });
190
}
191
});
192
193
test('environment variable with choices but empty value should create pick input (GitHub issue #266106)', () => {
194
const manifest: IGalleryMcpServerConfiguration = {
195
packages: [{
196
registryType: RegistryType.NODE,
197
transport: { type: TransportType.STDIO },
198
identifier: 'test-server',
199
version: '1.0.0',
200
environmentVariables: [{
201
name: 'SSL_MODE',
202
value: '', // Empty value should create input variable
203
description: 'SSL connection mode',
204
default: 'prefer',
205
choices: ['disable', 'prefer', 'require']
206
}]
207
}]
208
};
209
210
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
211
212
// BUG: Currently this creates env with empty string instead of input variable
213
// Should create a pick input variable since choices are provided
214
assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);
215
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].id, 'SSL_MODE');
216
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].description, 'SSL connection mode');
217
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].default, 'prefer');
218
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].type, McpServerVariableType.PICK);
219
assert.deepStrictEqual(result.mcpServerConfiguration.inputs?.[0].options, ['disable', 'prefer', 'require']);
220
221
// Environment should use input variable interpolation
222
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
223
assert.deepStrictEqual(result.mcpServerConfiguration.config.env, { 'SSL_MODE': '${input:SSL_MODE}' });
224
}
225
});
226
227
test('NPM package with package arguments', () => {
228
const manifest: IGalleryMcpServerConfiguration = {
229
packages: [{
230
registryType: RegistryType.NODE,
231
transport: { type: TransportType.STDIO },
232
identifier: 'snyk',
233
version: '1.1298.0',
234
packageArguments: [
235
{ type: 'positional', value: 'mcp', valueHint: 'command', isRepeated: false },
236
{
237
type: 'named',
238
name: '-t',
239
value: 'stdio',
240
isRepeated: false
241
}
242
]
243
}]
244
};
245
246
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
247
248
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);
249
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
250
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]', 'mcp', '-t', 'stdio']);
251
}
252
});
253
});
254
255
suite('Python Package Tests', () => {
256
test('basic Python package configuration', () => {
257
const manifest: IGalleryMcpServerConfiguration = {
258
packages: [{
259
registryType: RegistryType.PYTHON,
260
transport: { type: TransportType.STDIO },
261
identifier: 'weather-mcp-server',
262
version: '0.5.0',
263
environmentVariables: [{
264
name: 'WEATHER_API_KEY',
265
value: 'test-key'
266
}, {
267
name: 'WEATHER_UNITS',
268
value: 'celsius'
269
}]
270
}]
271
};
272
273
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.PYTHON);
274
275
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);
276
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
277
assert.strictEqual(result.mcpServerConfiguration.config.command, 'uvx');
278
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]']);
279
assert.deepStrictEqual(result.mcpServerConfiguration.config.env, {
280
'WEATHER_API_KEY': 'test-key',
281
'WEATHER_UNITS': 'celsius'
282
});
283
}
284
});
285
286
test('Python package with custom registry URL', () => {
287
const manifest: IGalleryMcpServerConfiguration = {
288
packages: [{
289
registryType: RegistryType.PYTHON,
290
registryBaseUrl: 'https://custom-pypi.example.com/simple',
291
transport: { type: TransportType.STDIO },
292
identifier: 'internal-python-server',
293
version: '1.2.3'
294
}]
295
};
296
297
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.PYTHON);
298
299
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);
300
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
301
assert.strictEqual(result.mcpServerConfiguration.config.command, 'uvx');
302
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [
303
'--index-url', 'https://custom-pypi.example.com/simple',
304
'[email protected]'
305
]);
306
}
307
});
308
309
test('Python package without version', () => {
310
const manifest: IGalleryMcpServerConfiguration = {
311
packages: [{
312
registryType: RegistryType.PYTHON,
313
transport: { type: TransportType.STDIO },
314
identifier: 'weather-mcp-server',
315
version: ''
316
}]
317
};
318
319
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.PYTHON);
320
321
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
322
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['weather-mcp-server']);
323
}
324
});
325
});
326
327
suite('Docker Package Tests', () => {
328
test('basic Docker package configuration', () => {
329
const manifest: IGalleryMcpServerConfiguration = {
330
packages: [{
331
registryType: RegistryType.DOCKER,
332
transport: { type: TransportType.STDIO },
333
identifier: 'mcp/filesystem',
334
version: '1.0.2',
335
runtimeArguments: [{
336
type: 'named',
337
name: '--mount',
338
value: 'type=bind,src=/host/path,dst=/container/path',
339
isRepeated: false
340
}],
341
environmentVariables: [{
342
name: 'LOG_LEVEL',
343
value: 'info'
344
}],
345
packageArguments: [{
346
type: 'positional',
347
value: '/project',
348
valueHint: 'directory',
349
isRepeated: false
350
}]
351
}]
352
};
353
354
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.DOCKER);
355
356
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);
357
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
358
assert.strictEqual(result.mcpServerConfiguration.config.command, 'docker');
359
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [
360
'run', '-i', '--rm',
361
'--mount', 'type=bind,src=/host/path,dst=/container/path',
362
'-e', 'LOG_LEVEL',
363
'mcp/filesystem:1.0.2',
364
'/project'
365
]);
366
assert.deepStrictEqual(result.mcpServerConfiguration.config.env, { 'LOG_LEVEL': 'info' });
367
}
368
});
369
370
test('Docker package with custom registry URL', () => {
371
const manifest: IGalleryMcpServerConfiguration = {
372
packages: [{
373
registryType: RegistryType.DOCKER,
374
registryBaseUrl: 'registry.company.com',
375
transport: { type: TransportType.STDIO },
376
identifier: 'internal/mcp-server',
377
version: '3.2.1'
378
}]
379
};
380
381
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.DOCKER);
382
383
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);
384
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
385
assert.strictEqual(result.mcpServerConfiguration.config.command, 'docker');
386
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [
387
'run', '-i', '--rm',
388
'registry.company.com/internal/mcp-server:3.2.1'
389
]);
390
}
391
});
392
393
test('Docker package with variables in runtime arguments', () => {
394
const manifest: IGalleryMcpServerConfiguration = {
395
packages: [{
396
registryType: RegistryType.DOCKER,
397
transport: { type: TransportType.STDIO },
398
identifier: 'example/database-manager-mcp',
399
version: '3.1.0',
400
runtimeArguments: [{
401
type: 'named',
402
name: '-e',
403
value: 'DB_TYPE={db_type}',
404
isRepeated: false,
405
variables: {
406
db_type: {
407
description: 'Type of database',
408
choices: ['postgres', 'mysql', 'mongodb', 'redis'],
409
isRequired: true
410
}
411
}
412
}]
413
}]
414
};
415
416
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.DOCKER);
417
418
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);
419
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
420
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [
421
'run', '-i', '--rm',
422
'-e', 'DB_TYPE=${input:db_type}',
423
'example/database-manager-mcp:3.1.0'
424
]);
425
}
426
assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);
427
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].id, 'db_type');
428
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].type, McpServerVariableType.PICK);
429
assert.deepStrictEqual(result.mcpServerConfiguration.inputs?.[0].options, ['postgres', 'mysql', 'mongodb', 'redis']);
430
});
431
432
test('Docker package arguments without values should create input variables (GitHub issue #266106)', () => {
433
const manifest: IGalleryMcpServerConfiguration = {
434
packages: [{
435
registryType: RegistryType.DOCKER,
436
transport: { type: TransportType.STDIO },
437
identifier: 'example/database-manager-mcp',
438
version: '3.1.0',
439
packageArguments: [{
440
type: 'named',
441
name: '--host',
442
description: 'Database host',
443
default: 'localhost',
444
isRequired: true,
445
isRepeated: false
446
// Note: No 'value' field - should create input variable
447
}, {
448
type: 'positional',
449
valueHint: 'database_name',
450
description: 'Name of the database to connect to',
451
isRequired: true,
452
isRepeated: false
453
// Note: No 'value' field - should create input variable
454
}]
455
}]
456
};
457
458
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.DOCKER);
459
460
// BUG: Currently named args without value are ignored, positional uses value_hint as literal
461
// Should create input variables for both arguments
462
assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 2);
463
464
const hostInput = result.mcpServerConfiguration.inputs?.find((i: IMcpServerVariable) => i.id === 'host');
465
assert.strictEqual(hostInput?.description, 'Database host');
466
assert.strictEqual(hostInput?.default, 'localhost');
467
assert.strictEqual(hostInput?.type, McpServerVariableType.PROMPT);
468
469
const dbNameInput = result.mcpServerConfiguration.inputs?.find((i: IMcpServerVariable) => i.id === 'database_name');
470
assert.strictEqual(dbNameInput?.description, 'Name of the database to connect to');
471
assert.strictEqual(dbNameInput?.type, McpServerVariableType.PROMPT);
472
473
// Args should use input variable interpolation
474
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
475
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [
476
'run', '-i', '--rm',
477
'example/database-manager-mcp:3.1.0',
478
'--host', '${input:host}',
479
'${input:database_name}'
480
]);
481
}
482
});
483
484
test('Docker Hub backward compatibility', () => {
485
const manifest: IGalleryMcpServerConfiguration = {
486
packages: [{
487
registryType: RegistryType.DOCKER,
488
identifier: 'example/test-image',
489
transport: { type: TransportType.STDIO },
490
version: '1.0.0'
491
}]
492
};
493
494
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.DOCKER);
495
496
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);
497
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
498
assert.strictEqual(result.mcpServerConfiguration.config.command, 'docker');
499
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [
500
'run', '-i', '--rm',
501
'example/test-image:1.0.0'
502
]);
503
}
504
});
505
});
506
507
suite('NuGet Package Tests', () => {
508
test('basic NuGet package configuration', () => {
509
const manifest: IGalleryMcpServerConfiguration = {
510
packages: [{
511
registryType: RegistryType.NUGET,
512
transport: { type: TransportType.STDIO },
513
identifier: 'Knapcode.SampleMcpServer',
514
version: '0.5.0',
515
environmentVariables: [{
516
name: 'WEATHER_CHOICES',
517
value: 'sunny,cloudy,rainy'
518
}]
519
}]
520
};
521
522
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NUGET);
523
524
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);
525
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
526
assert.strictEqual(result.mcpServerConfiguration.config.command, 'dnx');
527
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]', '--yes']);
528
assert.deepStrictEqual(result.mcpServerConfiguration.config.env, { 'WEATHER_CHOICES': 'sunny,cloudy,rainy' });
529
}
530
});
531
532
test('NuGet package with custom registry URL', () => {
533
const manifest: IGalleryMcpServerConfiguration = {
534
packages: [{
535
registryType: RegistryType.NUGET,
536
registryBaseUrl: 'https://nuget.company.com/v3/index.json',
537
transport: { type: TransportType.STDIO },
538
identifier: 'Company.Internal.McpServer',
539
version: '4.5.6'
540
}]
541
};
542
543
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NUGET);
544
545
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);
546
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
547
assert.strictEqual(result.mcpServerConfiguration.config.command, 'dnx');
548
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [
549
'[email protected]',
550
'--yes',
551
'--source', 'https://nuget.company.com/v3/index.json'
552
]);
553
}
554
});
555
556
test('NuGet package with package arguments', () => {
557
const manifest: IGalleryMcpServerConfiguration = {
558
packages: [{
559
registryType: RegistryType.NUGET,
560
transport: { type: TransportType.STDIO },
561
identifier: 'Knapcode.SampleMcpServer',
562
version: '0.4.0-beta',
563
packageArguments: [{
564
type: 'positional',
565
value: 'mcp',
566
valueHint: 'command',
567
isRepeated: false
568
}, {
569
type: 'positional',
570
value: 'start',
571
valueHint: 'action',
572
isRepeated: false
573
}]
574
}]
575
};
576
577
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NUGET);
578
579
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
580
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [
581
'[email protected]',
582
'--yes',
583
'--',
584
'mcp',
585
'start'
586
]);
587
}
588
});
589
});
590
591
suite('Remote Server Tests', () => {
592
test('SSE remote server configuration', () => {
593
const manifest: IGalleryMcpServerConfiguration = {
594
remotes: [{
595
type: TransportType.SSE,
596
url: 'http://mcp-fs.anonymous.modelcontextprotocol.io/sse'
597
}]
598
};
599
600
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.REMOTE);
601
602
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.REMOTE);
603
if (result.mcpServerConfiguration.config.type === McpServerType.REMOTE) {
604
assert.strictEqual(result.mcpServerConfiguration.config.url, 'http://mcp-fs.anonymous.modelcontextprotocol.io/sse');
605
assert.strictEqual(result.mcpServerConfiguration.config.headers, undefined);
606
}
607
});
608
609
test('SSE remote server with headers and variables', () => {
610
const manifest: IGalleryMcpServerConfiguration = {
611
remotes: [{
612
type: TransportType.SSE,
613
url: 'https://mcp.anonymous.modelcontextprotocol.io/sse',
614
headers: [{
615
name: 'X-API-Key',
616
value: '{api_key}',
617
variables: {
618
api_key: {
619
description: 'API key for authentication',
620
isRequired: true,
621
isSecret: true
622
}
623
}
624
}, {
625
name: 'X-Region',
626
value: 'us-east-1'
627
}]
628
}]
629
};
630
631
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.REMOTE);
632
633
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.REMOTE);
634
if (result.mcpServerConfiguration.config.type === McpServerType.REMOTE) {
635
assert.deepStrictEqual(result.mcpServerConfiguration.config.headers, {
636
'X-API-Key': '${input:api_key}',
637
'X-Region': 'us-east-1'
638
});
639
}
640
assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);
641
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].id, 'api_key');
642
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].password, true);
643
});
644
645
test('streamable HTTP remote server', () => {
646
const manifest: IGalleryMcpServerConfiguration = {
647
remotes: [{
648
type: TransportType.STREAMABLE_HTTP,
649
url: 'https://mcp.anonymous.modelcontextprotocol.io/http'
650
}]
651
};
652
653
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.REMOTE);
654
655
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.REMOTE);
656
if (result.mcpServerConfiguration.config.type === McpServerType.REMOTE) {
657
assert.strictEqual(result.mcpServerConfiguration.config.url, 'https://mcp.anonymous.modelcontextprotocol.io/http');
658
}
659
});
660
661
test('remote headers without values should create input variables', () => {
662
const manifest: IGalleryMcpServerConfiguration = {
663
remotes: [{
664
type: TransportType.SSE,
665
url: 'https://api.example.com/mcp',
666
headers: [{
667
name: 'Authorization',
668
description: 'API token for authentication',
669
isSecret: true,
670
isRequired: true
671
// Note: No 'value' field - should create input variable
672
}, {
673
name: 'X-Custom-Header',
674
description: 'Custom header value',
675
default: 'default-value',
676
choices: ['option1', 'option2', 'option3']
677
// Note: No 'value' field - should create input variable with choices
678
}]
679
}]
680
};
681
682
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.REMOTE);
683
684
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.REMOTE);
685
if (result.mcpServerConfiguration.config.type === McpServerType.REMOTE) {
686
assert.strictEqual(result.mcpServerConfiguration.config.url, 'https://api.example.com/mcp');
687
assert.deepStrictEqual(result.mcpServerConfiguration.config.headers, {
688
'Authorization': '${input:Authorization}',
689
'X-Custom-Header': '${input:X-Custom-Header}'
690
});
691
}
692
693
// Should create input variables for headers without values
694
assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 2);
695
696
const authInput = result.mcpServerConfiguration.inputs?.find((i: IMcpServerVariable) => i.id === 'Authorization');
697
assert.strictEqual(authInput?.description, 'API token for authentication');
698
assert.strictEqual(authInput?.password, true);
699
assert.strictEqual(authInput?.type, McpServerVariableType.PROMPT);
700
701
const customInput = result.mcpServerConfiguration.inputs?.find((i: IMcpServerVariable) => i.id === 'X-Custom-Header');
702
assert.strictEqual(customInput?.description, 'Custom header value');
703
assert.strictEqual(customInput?.default, 'default-value');
704
assert.strictEqual(customInput?.type, McpServerVariableType.PICK);
705
assert.deepStrictEqual(customInput?.options, ['option1', 'option2', 'option3']);
706
});
707
});
708
709
suite('Variable Interpolation Tests', () => {
710
test('multiple variables in single value', () => {
711
const manifest: IGalleryMcpServerConfiguration = {
712
packages: [{
713
registryType: RegistryType.NODE,
714
identifier: 'test-server',
715
transport: { type: TransportType.STDIO },
716
version: '1.0.0',
717
environmentVariables: [{
718
name: 'CONNECTION_STRING',
719
value: 'server={host};port={port};database={db_name}',
720
variables: {
721
host: {
722
description: 'Database host',
723
default: 'localhost'
724
},
725
port: {
726
description: 'Database port',
727
format: 'number',
728
default: '5432'
729
},
730
db_name: {
731
description: 'Database name',
732
isRequired: true
733
}
734
}
735
}]
736
}]
737
};
738
739
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
740
741
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
742
assert.deepStrictEqual(result.mcpServerConfiguration.config.env, {
743
'CONNECTION_STRING': 'server=${input:host};port=${input:port};database=${input:db_name}'
744
});
745
}
746
assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 3);
747
748
const hostInput = result.mcpServerConfiguration.inputs?.find((i: IMcpServerVariable) => i.id === 'host');
749
assert.strictEqual(hostInput?.default, 'localhost');
750
assert.strictEqual(hostInput?.type, McpServerVariableType.PROMPT);
751
752
const portInput = result.mcpServerConfiguration.inputs?.find((i: IMcpServerVariable) => i.id === 'port');
753
assert.strictEqual(portInput?.default, '5432');
754
755
const dbNameInput = result.mcpServerConfiguration.inputs?.find((i: IMcpServerVariable) => i.id === 'db_name');
756
assert.strictEqual(dbNameInput?.description, 'Database name');
757
});
758
759
test('variable with choices creates pick input', () => {
760
const manifest: IGalleryMcpServerConfiguration = {
761
packages: [{
762
registryType: RegistryType.NODE,
763
identifier: 'test-server',
764
transport: { type: TransportType.STDIO },
765
version: '1.0.0',
766
runtimeArguments: [{
767
type: 'named',
768
name: '--log-level',
769
value: '{level}',
770
isRepeated: false,
771
variables: {
772
level: {
773
description: 'Log level',
774
choices: ['debug', 'info', 'warn', 'error'],
775
default: 'info'
776
}
777
}
778
}]
779
}]
780
};
781
782
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
783
784
assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);
785
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].type, McpServerVariableType.PICK);
786
assert.deepStrictEqual(result.mcpServerConfiguration.inputs?.[0].options, ['debug', 'info', 'warn', 'error']);
787
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].default, 'info');
788
});
789
790
test('variables in package arguments', () => {
791
const manifest: IGalleryMcpServerConfiguration = {
792
packages: [{
793
registryType: RegistryType.DOCKER,
794
identifier: 'test-image',
795
transport: { type: TransportType.STDIO },
796
version: '1.0.0',
797
packageArguments: [{
798
type: 'named',
799
name: '--host',
800
value: '{db_host}',
801
isRepeated: false,
802
variables: {
803
db_host: {
804
description: 'Database host',
805
default: 'localhost'
806
}
807
}
808
}, {
809
type: 'positional',
810
value: '{database_name}',
811
valueHint: 'database_name',
812
isRepeated: false,
813
variables: {
814
database_name: {
815
description: 'Name of the database to connect to',
816
isRequired: true
817
}
818
}
819
}]
820
}]
821
};
822
823
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.DOCKER);
824
825
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
826
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [
827
'run', '-i', '--rm',
828
'test-image:1.0.0',
829
'--host', '${input:db_host}',
830
'${input:database_name}'
831
]);
832
}
833
assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 2);
834
});
835
836
test('positional arguments with value_hint should create input variables (GitHub issue #266106)', () => {
837
const manifest: IGalleryMcpServerConfiguration = {
838
packages: [{
839
registryType: RegistryType.NODE,
840
identifier: '@example/math-tool',
841
transport: { type: TransportType.STDIO },
842
version: '2.0.1',
843
packageArguments: [{
844
type: 'positional',
845
valueHint: 'calculation_type',
846
description: 'Type of calculation to enable',
847
isRequired: true,
848
isRepeated: false
849
// Note: No 'value' field, only value_hint - should create input variable
850
}]
851
}]
852
};
853
854
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
855
856
// BUG: Currently value_hint is used as literal value instead of creating input variable
857
// Should create input variable instead
858
assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);
859
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].id, 'calculation_type');
860
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].description, 'Type of calculation to enable');
861
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].type, McpServerVariableType.PROMPT);
862
863
// Args should use input variable interpolation
864
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
865
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [
866
'@example/[email protected]',
867
'${input:calculation_type}'
868
]);
869
}
870
});
871
});
872
873
suite('Edge Cases and Error Handling', () => {
874
test('empty manifest should throw error', () => {
875
const manifest: IGalleryMcpServerConfiguration = {};
876
877
assert.throws(() => {
878
service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
879
}, /No server package found/);
880
});
881
882
test('manifest with no matching package type should use first package', () => {
883
const manifest: IGalleryMcpServerConfiguration = {
884
packages: [{
885
registryType: RegistryType.PYTHON,
886
transport: { type: TransportType.STDIO },
887
identifier: 'python-server',
888
version: '1.0.0'
889
}]
890
};
891
892
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
893
894
assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);
895
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
896
assert.strictEqual(result.mcpServerConfiguration.config.command, 'uvx'); // Python command since that's the package type
897
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]']);
898
}
899
});
900
901
test('manifest with matching package type should use that package', () => {
902
const manifest: IGalleryMcpServerConfiguration = {
903
packages: [{
904
registryType: RegistryType.PYTHON,
905
transport: { type: TransportType.STDIO },
906
identifier: 'python-server',
907
version: '1.0.0'
908
}, {
909
registryType: RegistryType.NODE,
910
transport: { type: TransportType.STDIO },
911
identifier: 'node-server',
912
version: '2.0.0'
913
}]
914
};
915
916
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
917
918
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
919
assert.strictEqual(result.mcpServerConfiguration.config.command, 'npx');
920
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]']);
921
}
922
});
923
924
test('undefined environment variables should be omitted', () => {
925
const manifest: IGalleryMcpServerConfiguration = {
926
packages: [{
927
registryType: RegistryType.NODE,
928
transport: { type: TransportType.STDIO },
929
identifier: 'test-server',
930
version: '1.0.0'
931
}]
932
};
933
934
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
935
936
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
937
assert.strictEqual(result.mcpServerConfiguration.config.env, undefined);
938
}
939
});
940
941
test('named argument without value should only add name', () => {
942
const manifest: IGalleryMcpServerConfiguration = {
943
packages: [{
944
registryType: RegistryType.NODE,
945
transport: { type: TransportType.STDIO },
946
identifier: 'test-server',
947
version: '1.0.0',
948
runtimeArguments: [{
949
type: 'named',
950
name: '--verbose',
951
isRepeated: false
952
}]
953
}]
954
};
955
956
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
957
958
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
959
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['--verbose', '[email protected]']);
960
}
961
});
962
963
test('positional argument with undefined value should use value_hint', () => {
964
const manifest: IGalleryMcpServerConfiguration = {
965
packages: [{
966
registryType: RegistryType.NODE,
967
identifier: 'test-server',
968
transport: { type: TransportType.STDIO },
969
version: '1.0.0',
970
packageArguments: [{
971
type: 'positional',
972
valueHint: 'target_directory',
973
isRepeated: false
974
}]
975
}]
976
};
977
978
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
979
980
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
981
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]', 'target_directory']);
982
}
983
});
984
985
test('named argument with no name should generate notice', () => {
986
const manifest = {
987
packages: [{
988
registryType: RegistryType.NODE,
989
identifier: 'test-server',
990
transport: { type: TransportType.STDIO },
991
version: '1.0.0',
992
runtimeArguments: [{
993
type: 'named',
994
value: 'some-value',
995
isRepeated: false
996
}]
997
}]
998
};
999
1000
const result = service.getMcpServerConfigurationFromManifest(manifest as IGalleryMcpServerConfiguration, RegistryType.NODE);
1001
1002
// Should generate a notice about the missing name
1003
assert.strictEqual(result.notices.length, 1);
1004
assert.ok(result.notices[0].includes('Named argument is missing a name'));
1005
assert.ok(result.notices[0].includes('some-value')); // Should include the argument details in JSON format
1006
1007
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
1008
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]']);
1009
}
1010
});
1011
1012
test('named argument with empty name should generate notice', () => {
1013
const manifest: IGalleryMcpServerConfiguration = {
1014
packages: [{
1015
registryType: RegistryType.NODE,
1016
identifier: 'test-server',
1017
transport: { type: TransportType.STDIO },
1018
version: '1.0.0',
1019
runtimeArguments: [{
1020
type: 'named',
1021
name: '',
1022
value: 'some-value',
1023
isRepeated: false
1024
}]
1025
}]
1026
};
1027
1028
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
1029
1030
// Should generate a notice about the missing name
1031
assert.strictEqual(result.notices.length, 1);
1032
assert.ok(result.notices[0].includes('Named argument is missing a name'));
1033
assert.ok(result.notices[0].includes('some-value')); // Should include the argument details in JSON format
1034
1035
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
1036
assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]']);
1037
}
1038
});
1039
});
1040
1041
suite('Variable Processing Order', () => {
1042
test('should use explicit variables instead of auto-generating when both are possible', () => {
1043
const manifest: IGalleryMcpServerConfiguration = {
1044
packages: [{
1045
registryType: RegistryType.NODE,
1046
identifier: 'test-server',
1047
transport: { type: TransportType.STDIO },
1048
version: '1.0.0',
1049
environmentVariables: [{
1050
name: 'API_KEY',
1051
value: 'Bearer {api_key}',
1052
description: 'Should not be used', // This should be ignored since we have explicit variables
1053
variables: {
1054
api_key: {
1055
description: 'Your API key',
1056
isSecret: true
1057
}
1058
}
1059
}]
1060
}]
1061
};
1062
1063
const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);
1064
1065
assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);
1066
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].id, 'api_key');
1067
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].description, 'Your API key');
1068
assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].password, true);
1069
1070
if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {
1071
assert.strictEqual(result.mcpServerConfiguration.config.env?.['API_KEY'], 'Bearer ${input:api_key}');
1072
}
1073
});
1074
});
1075
});
1076
1077