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