Path: blob/main/src/vs/platform/mcp/test/common/mcpManagementService.test.ts
5245 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';7import { AbstractCommonMcpManagementService } from '../../common/mcpManagementService.js';8import { IGalleryMcpServer, IGalleryMcpServerConfiguration, IInstallableMcpServer, ILocalMcpServer, InstallOptions, RegistryType, TransportType, UninstallOptions } from '../../common/mcpManagement.js';9import { McpServerType, McpServerVariableType, IMcpServerVariable } from '../../common/mcpPlatformTypes.js';10import { IMarkdownString } from '../../../../base/common/htmlContent.js';11import { Event } from '../../../../base/common/event.js';12import { URI } from '../../../../base/common/uri.js';13import { NullLogService } from '../../../log/common/log.js';1415class TestMcpManagementService extends AbstractCommonMcpManagementService {1617override onInstallMcpServer = Event.None;18override onDidInstallMcpServers = Event.None;19override onDidUpdateMcpServers = Event.None;20override onUninstallMcpServer = Event.None;21override onDidUninstallMcpServer = Event.None;2223override getInstalled(mcpResource?: URI): Promise<ILocalMcpServer[]> {24throw new Error('Method not implemented.');25}26override install(server: IInstallableMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {27throw new Error('Method not implemented.');28}29override installFromGallery(server: IGalleryMcpServer, options?: InstallOptions): Promise<ILocalMcpServer> {30throw new Error('Method not implemented.');31}32override updateMetadata(local: ILocalMcpServer, server: IGalleryMcpServer, profileLocation?: URI): Promise<ILocalMcpServer> {33throw new Error('Method not implemented.');34}35override uninstall(server: ILocalMcpServer, options?: UninstallOptions): Promise<void> {36throw new Error('Method not implemented.');37}3839override canInstall(server: IGalleryMcpServer | IInstallableMcpServer): true | IMarkdownString {40throw new Error('Not supported');41}42}4344suite('McpManagementService - getMcpServerConfigurationFromManifest', () => {45let service: TestMcpManagementService;4647setup(() => {48service = new TestMcpManagementService(new NullLogService());49});5051teardown(() => {52service.dispose();53});5455ensureNoDisposablesAreLeakedInTestSuite();5657suite('NPM Package Tests', () => {58test('basic NPM package configuration', () => {59const manifest: IGalleryMcpServerConfiguration = {60packages: [{61registryType: RegistryType.NODE,62identifier: '@modelcontextprotocol/server-brave-search',63transport: { type: TransportType.STDIO },64version: '1.0.2',65environmentVariables: [{66name: 'BRAVE_API_KEY',67value: 'test-key'68}]69}]70};7172const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);7374assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);75if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {76assert.strictEqual(result.mcpServerConfiguration.config.command, 'npx');77assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['@modelcontextprotocol/[email protected]']);78assert.deepStrictEqual(result.mcpServerConfiguration.config.env, { 'BRAVE_API_KEY': 'test-key' });79}80assert.strictEqual(result.mcpServerConfiguration.inputs, undefined);81});8283test('NPM package with custom registry URL', () => {84const manifest: IGalleryMcpServerConfiguration = {85packages: [{86registryType: RegistryType.NODE,87registryBaseUrl: 'https://custom-registry.example.com',88identifier: '@company/internal-package',89transport: { type: TransportType.STDIO },90version: '2.1.0'91}]92};9394const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);9596assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);97if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {98assert.strictEqual(result.mcpServerConfiguration.config.command, 'npx');99assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [100'--registry', 'https://custom-registry.example.com',101'@company/[email protected]'102]);103}104});105106test('NPM package without version', () => {107const manifest: IGalleryMcpServerConfiguration = {108packages: [{109registryType: RegistryType.NODE,110identifier: '@modelcontextprotocol/everything',111version: '',112transport: { type: TransportType.STDIO }113}]114};115116const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);117118assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);119if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {120assert.strictEqual(result.mcpServerConfiguration.config.command, 'npx');121assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['@modelcontextprotocol/everything']);122}123});124125test('NPM package with environment variables containing variables', () => {126const manifest: IGalleryMcpServerConfiguration = {127packages: [{128registryType: RegistryType.NODE,129transport: { type: TransportType.STDIO },130identifier: 'test-server',131version: '1.0.0',132environmentVariables: [{133name: 'API_KEY',134value: 'key-{api_token}',135variables: {136api_token: {137description: 'Your API token',138isSecret: true,139isRequired: true140}141}142}]143}]144};145146const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);147148assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);149if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {150assert.deepStrictEqual(result.mcpServerConfiguration.config.env, { 'API_KEY': 'key-${input:api_token}' });151}152assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);153assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].id, 'api_token');154assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].type, McpServerVariableType.PROMPT);155assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].description, 'Your API token');156assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].password, true);157});158159test('environment variable with empty value should create input variable (GitHub issue #266106)', () => {160const manifest: IGalleryMcpServerConfiguration = {161packages: [{162registryType: RegistryType.NODE,163transport: { type: TransportType.STDIO },164identifier: '@modelcontextprotocol/server-brave-search',165version: '1.0.2',166environmentVariables: [{167name: 'BRAVE_API_KEY',168value: '', // Empty value should create input variable169description: 'Brave Search API Key',170isRequired: true,171isSecret: true172}]173}]174};175176const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);177178// BUG: Currently this creates env with empty string instead of input variable179// Should create an input variable since no meaningful value is provided180assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);181assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].id, 'BRAVE_API_KEY');182assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].description, 'Brave Search API Key');183assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].password, true);184assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].type, McpServerVariableType.PROMPT);185186// Environment should use input variable interpolation187if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {188assert.deepStrictEqual(result.mcpServerConfiguration.config.env, { 'BRAVE_API_KEY': '${input:BRAVE_API_KEY}' });189}190});191192test('environment variable with choices but empty value should create pick input (GitHub issue #266106)', () => {193const manifest: IGalleryMcpServerConfiguration = {194packages: [{195registryType: RegistryType.NODE,196transport: { type: TransportType.STDIO },197identifier: 'test-server',198version: '1.0.0',199environmentVariables: [{200name: 'SSL_MODE',201value: '', // Empty value should create input variable202description: 'SSL connection mode',203default: 'prefer',204choices: ['disable', 'prefer', 'require']205}]206}]207};208209const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);210211// BUG: Currently this creates env with empty string instead of input variable212// Should create a pick input variable since choices are provided213assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);214assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].id, 'SSL_MODE');215assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].description, 'SSL connection mode');216assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].default, 'prefer');217assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].type, McpServerVariableType.PICK);218assert.deepStrictEqual(result.mcpServerConfiguration.inputs?.[0].options, ['disable', 'prefer', 'require']);219220// Environment should use input variable interpolation221if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {222assert.deepStrictEqual(result.mcpServerConfiguration.config.env, { 'SSL_MODE': '${input:SSL_MODE}' });223}224});225226test('NPM package with package arguments', () => {227const manifest: IGalleryMcpServerConfiguration = {228packages: [{229registryType: RegistryType.NODE,230transport: { type: TransportType.STDIO },231identifier: 'snyk',232version: '1.1298.0',233packageArguments: [234{ type: 'positional', value: 'mcp', valueHint: 'command', isRepeated: false },235{236type: 'named',237name: '-t',238value: 'stdio',239isRepeated: false240}241]242}]243};244245const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);246247assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);248if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {249assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]', 'mcp', '-t', 'stdio']);250}251});252});253254suite('Python Package Tests', () => {255test('basic Python package configuration', () => {256const manifest: IGalleryMcpServerConfiguration = {257packages: [{258registryType: RegistryType.PYTHON,259transport: { type: TransportType.STDIO },260identifier: 'weather-mcp-server',261version: '0.5.0',262environmentVariables: [{263name: 'WEATHER_API_KEY',264value: 'test-key'265}, {266name: 'WEATHER_UNITS',267value: 'celsius'268}]269}]270};271272const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.PYTHON);273274assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);275if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {276assert.strictEqual(result.mcpServerConfiguration.config.command, 'uvx');277assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]']);278assert.deepStrictEqual(result.mcpServerConfiguration.config.env, {279'WEATHER_API_KEY': 'test-key',280'WEATHER_UNITS': 'celsius'281});282}283});284285test('Python package with custom registry URL', () => {286const manifest: IGalleryMcpServerConfiguration = {287packages: [{288registryType: RegistryType.PYTHON,289registryBaseUrl: 'https://custom-pypi.example.com/simple',290transport: { type: TransportType.STDIO },291identifier: 'internal-python-server',292version: '1.2.3'293}]294};295296const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.PYTHON);297298assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);299if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {300assert.strictEqual(result.mcpServerConfiguration.config.command, 'uvx');301assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [302'--index-url', 'https://custom-pypi.example.com/simple',303'[email protected]'304]);305}306});307308test('Python package without version', () => {309const manifest: IGalleryMcpServerConfiguration = {310packages: [{311registryType: RegistryType.PYTHON,312transport: { type: TransportType.STDIO },313identifier: 'weather-mcp-server',314version: ''315}]316};317318const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.PYTHON);319320if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {321assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['weather-mcp-server']);322}323});324});325326suite('Docker Package Tests', () => {327test('basic Docker package configuration', () => {328const manifest: IGalleryMcpServerConfiguration = {329packages: [{330registryType: RegistryType.DOCKER,331transport: { type: TransportType.STDIO },332identifier: 'mcp/filesystem',333version: '1.0.2',334runtimeArguments: [{335type: 'named',336name: '--mount',337value: 'type=bind,src=/host/path,dst=/container/path',338isRepeated: false339}],340environmentVariables: [{341name: 'LOG_LEVEL',342value: 'info'343}],344packageArguments: [{345type: 'positional',346value: '/project',347valueHint: 'directory',348isRepeated: false349}]350}]351};352353const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.DOCKER);354355assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);356if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {357assert.strictEqual(result.mcpServerConfiguration.config.command, 'docker');358assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [359'run', '-i', '--rm',360'--mount', 'type=bind,src=/host/path,dst=/container/path',361'-e', 'LOG_LEVEL',362'mcp/filesystem:1.0.2',363'/project'364]);365assert.deepStrictEqual(result.mcpServerConfiguration.config.env, { 'LOG_LEVEL': 'info' });366}367});368369test('Docker package with custom registry URL', () => {370const manifest: IGalleryMcpServerConfiguration = {371packages: [{372registryType: RegistryType.DOCKER,373registryBaseUrl: 'registry.company.com',374transport: { type: TransportType.STDIO },375identifier: 'internal/mcp-server',376version: '3.2.1'377}]378};379380const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.DOCKER);381382assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);383if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {384assert.strictEqual(result.mcpServerConfiguration.config.command, 'docker');385assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [386'run', '-i', '--rm',387'registry.company.com/internal/mcp-server:3.2.1'388]);389}390});391392test('Docker package with variables in runtime arguments', () => {393const manifest: IGalleryMcpServerConfiguration = {394packages: [{395registryType: RegistryType.DOCKER,396transport: { type: TransportType.STDIO },397identifier: 'example/database-manager-mcp',398version: '3.1.0',399runtimeArguments: [{400type: 'named',401name: '-e',402value: 'DB_TYPE={db_type}',403isRepeated: false,404variables: {405db_type: {406description: 'Type of database',407choices: ['postgres', 'mysql', 'mongodb', 'redis'],408isRequired: true409}410}411}]412}]413};414415const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.DOCKER);416417assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);418if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {419assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [420'run', '-i', '--rm',421'-e', 'DB_TYPE=${input:db_type}',422'example/database-manager-mcp:3.1.0'423]);424}425assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);426assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].id, 'db_type');427assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].type, McpServerVariableType.PICK);428assert.deepStrictEqual(result.mcpServerConfiguration.inputs?.[0].options, ['postgres', 'mysql', 'mongodb', 'redis']);429});430431test('Docker package arguments without values should create input variables (GitHub issue #266106)', () => {432const manifest: IGalleryMcpServerConfiguration = {433packages: [{434registryType: RegistryType.DOCKER,435transport: { type: TransportType.STDIO },436identifier: 'example/database-manager-mcp',437version: '3.1.0',438packageArguments: [{439type: 'named',440name: '--host',441description: 'Database host',442default: 'localhost',443isRequired: true,444isRepeated: false445// Note: No 'value' field - should create input variable446}, {447type: 'positional',448valueHint: 'database_name',449description: 'Name of the database to connect to',450isRequired: true,451isRepeated: false452// Note: No 'value' field - should create input variable453}]454}]455};456457const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.DOCKER);458459// BUG: Currently named args without value are ignored, positional uses value_hint as literal460// Should create input variables for both arguments461assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 2);462463const hostInput = result.mcpServerConfiguration.inputs?.find((i: IMcpServerVariable) => i.id === 'host');464assert.strictEqual(hostInput?.description, 'Database host');465assert.strictEqual(hostInput?.default, 'localhost');466assert.strictEqual(hostInput?.type, McpServerVariableType.PROMPT);467468const dbNameInput = result.mcpServerConfiguration.inputs?.find((i: IMcpServerVariable) => i.id === 'database_name');469assert.strictEqual(dbNameInput?.description, 'Name of the database to connect to');470assert.strictEqual(dbNameInput?.type, McpServerVariableType.PROMPT);471472// Args should use input variable interpolation473if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {474assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [475'run', '-i', '--rm',476'example/database-manager-mcp:3.1.0',477'--host', '${input:host}',478'${input:database_name}'479]);480}481});482483test('Docker Hub backward compatibility', () => {484const manifest: IGalleryMcpServerConfiguration = {485packages: [{486registryType: RegistryType.DOCKER,487identifier: 'example/test-image',488transport: { type: TransportType.STDIO },489version: '1.0.0'490}]491};492493const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.DOCKER);494495assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);496if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {497assert.strictEqual(result.mcpServerConfiguration.config.command, 'docker');498assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [499'run', '-i', '--rm',500'example/test-image:1.0.0'501]);502}503});504});505506suite('NuGet Package Tests', () => {507test('basic NuGet package configuration', () => {508const manifest: IGalleryMcpServerConfiguration = {509packages: [{510registryType: RegistryType.NUGET,511transport: { type: TransportType.STDIO },512identifier: 'Knapcode.SampleMcpServer',513version: '0.5.0',514environmentVariables: [{515name: 'WEATHER_CHOICES',516value: 'sunny,cloudy,rainy'517}]518}]519};520521const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NUGET);522523assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);524if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {525assert.strictEqual(result.mcpServerConfiguration.config.command, 'dnx');526assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]', '--yes']);527assert.deepStrictEqual(result.mcpServerConfiguration.config.env, { 'WEATHER_CHOICES': 'sunny,cloudy,rainy' });528}529});530531test('NuGet package with custom registry URL', () => {532const manifest: IGalleryMcpServerConfiguration = {533packages: [{534registryType: RegistryType.NUGET,535registryBaseUrl: 'https://nuget.company.com/v3/index.json',536transport: { type: TransportType.STDIO },537identifier: 'Company.Internal.McpServer',538version: '4.5.6'539}]540};541542const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NUGET);543544assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);545if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {546assert.strictEqual(result.mcpServerConfiguration.config.command, 'dnx');547assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [548'[email protected]',549'--yes',550'--source', 'https://nuget.company.com/v3/index.json'551]);552}553});554555test('NuGet package with package arguments', () => {556const manifest: IGalleryMcpServerConfiguration = {557packages: [{558registryType: RegistryType.NUGET,559transport: { type: TransportType.STDIO },560identifier: 'Knapcode.SampleMcpServer',561version: '0.4.0-beta',562packageArguments: [{563type: 'positional',564value: 'mcp',565valueHint: 'command',566isRepeated: false567}, {568type: 'positional',569value: 'start',570valueHint: 'action',571isRepeated: false572}]573}]574};575576const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NUGET);577578if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {579assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [580'[email protected]',581'--yes',582'--',583'mcp',584'start'585]);586}587});588});589590suite('Remote Server Tests', () => {591test('SSE remote server configuration', () => {592const manifest: IGalleryMcpServerConfiguration = {593remotes: [{594type: TransportType.SSE,595url: 'http://mcp-fs.anonymous.modelcontextprotocol.io/sse'596}]597};598599const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.REMOTE);600601assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.REMOTE);602if (result.mcpServerConfiguration.config.type === McpServerType.REMOTE) {603assert.strictEqual(result.mcpServerConfiguration.config.url, 'http://mcp-fs.anonymous.modelcontextprotocol.io/sse');604assert.strictEqual(result.mcpServerConfiguration.config.headers, undefined);605}606});607608test('SSE remote server with headers and variables', () => {609const manifest: IGalleryMcpServerConfiguration = {610remotes: [{611type: TransportType.SSE,612url: 'https://mcp.anonymous.modelcontextprotocol.io/sse',613headers: [{614name: 'X-API-Key',615value: '{api_key}',616variables: {617api_key: {618description: 'API key for authentication',619isRequired: true,620isSecret: true621}622}623}, {624name: 'X-Region',625value: 'us-east-1'626}]627}]628};629630const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.REMOTE);631632assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.REMOTE);633if (result.mcpServerConfiguration.config.type === McpServerType.REMOTE) {634assert.deepStrictEqual(result.mcpServerConfiguration.config.headers, {635'X-API-Key': '${input:api_key}',636'X-Region': 'us-east-1'637});638}639assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);640assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].id, 'api_key');641assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].password, true);642});643644test('streamable HTTP remote server', () => {645const manifest: IGalleryMcpServerConfiguration = {646remotes: [{647type: TransportType.STREAMABLE_HTTP,648url: 'https://mcp.anonymous.modelcontextprotocol.io/http'649}]650};651652const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.REMOTE);653654assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.REMOTE);655if (result.mcpServerConfiguration.config.type === McpServerType.REMOTE) {656assert.strictEqual(result.mcpServerConfiguration.config.url, 'https://mcp.anonymous.modelcontextprotocol.io/http');657}658});659660test('remote headers without values should create input variables', () => {661const manifest: IGalleryMcpServerConfiguration = {662remotes: [{663type: TransportType.SSE,664url: 'https://api.example.com/mcp',665headers: [{666name: 'Authorization',667description: 'API token for authentication',668isSecret: true,669isRequired: true670// Note: No 'value' field - should create input variable671}, {672name: 'X-Custom-Header',673description: 'Custom header value',674default: 'default-value',675choices: ['option1', 'option2', 'option3']676// Note: No 'value' field - should create input variable with choices677}]678}]679};680681const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.REMOTE);682683assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.REMOTE);684if (result.mcpServerConfiguration.config.type === McpServerType.REMOTE) {685assert.strictEqual(result.mcpServerConfiguration.config.url, 'https://api.example.com/mcp');686assert.deepStrictEqual(result.mcpServerConfiguration.config.headers, {687'Authorization': '${input:Authorization}',688'X-Custom-Header': '${input:X-Custom-Header}'689});690}691692// Should create input variables for headers without values693assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 2);694695const authInput = result.mcpServerConfiguration.inputs?.find((i: IMcpServerVariable) => i.id === 'Authorization');696assert.strictEqual(authInput?.description, 'API token for authentication');697assert.strictEqual(authInput?.password, true);698assert.strictEqual(authInput?.type, McpServerVariableType.PROMPT);699700const customInput = result.mcpServerConfiguration.inputs?.find((i: IMcpServerVariable) => i.id === 'X-Custom-Header');701assert.strictEqual(customInput?.description, 'Custom header value');702assert.strictEqual(customInput?.default, 'default-value');703assert.strictEqual(customInput?.type, McpServerVariableType.PICK);704assert.deepStrictEqual(customInput?.options, ['option1', 'option2', 'option3']);705});706});707708suite('Variable Interpolation Tests', () => {709test('multiple variables in single value', () => {710const manifest: IGalleryMcpServerConfiguration = {711packages: [{712registryType: RegistryType.NODE,713identifier: 'test-server',714transport: { type: TransportType.STDIO },715version: '1.0.0',716environmentVariables: [{717name: 'CONNECTION_STRING',718value: 'server={host};port={port};database={db_name}',719variables: {720host: {721description: 'Database host',722default: 'localhost'723},724port: {725description: 'Database port',726format: 'number',727default: '5432'728},729db_name: {730description: 'Database name',731isRequired: true732}733}734}]735}]736};737738const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);739740if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {741assert.deepStrictEqual(result.mcpServerConfiguration.config.env, {742'CONNECTION_STRING': 'server=${input:host};port=${input:port};database=${input:db_name}'743});744}745assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 3);746747const hostInput = result.mcpServerConfiguration.inputs?.find((i: IMcpServerVariable) => i.id === 'host');748assert.strictEqual(hostInput?.default, 'localhost');749assert.strictEqual(hostInput?.type, McpServerVariableType.PROMPT);750751const portInput = result.mcpServerConfiguration.inputs?.find((i: IMcpServerVariable) => i.id === 'port');752assert.strictEqual(portInput?.default, '5432');753754const dbNameInput = result.mcpServerConfiguration.inputs?.find((i: IMcpServerVariable) => i.id === 'db_name');755assert.strictEqual(dbNameInput?.description, 'Database name');756});757758test('variable with choices creates pick input', () => {759const manifest: IGalleryMcpServerConfiguration = {760packages: [{761registryType: RegistryType.NODE,762identifier: 'test-server',763transport: { type: TransportType.STDIO },764version: '1.0.0',765runtimeArguments: [{766type: 'named',767name: '--log-level',768value: '{level}',769isRepeated: false,770variables: {771level: {772description: 'Log level',773choices: ['debug', 'info', 'warn', 'error'],774default: 'info'775}776}777}]778}]779};780781const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);782783assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);784assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].type, McpServerVariableType.PICK);785assert.deepStrictEqual(result.mcpServerConfiguration.inputs?.[0].options, ['debug', 'info', 'warn', 'error']);786assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].default, 'info');787});788789test('variables in package arguments', () => {790const manifest: IGalleryMcpServerConfiguration = {791packages: [{792registryType: RegistryType.DOCKER,793identifier: 'test-image',794transport: { type: TransportType.STDIO },795version: '1.0.0',796packageArguments: [{797type: 'named',798name: '--host',799value: '{db_host}',800isRepeated: false,801variables: {802db_host: {803description: 'Database host',804default: 'localhost'805}806}807}, {808type: 'positional',809value: '{database_name}',810valueHint: 'database_name',811isRepeated: false,812variables: {813database_name: {814description: 'Name of the database to connect to',815isRequired: true816}817}818}]819}]820};821822const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.DOCKER);823824if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {825assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [826'run', '-i', '--rm',827'test-image:1.0.0',828'--host', '${input:db_host}',829'${input:database_name}'830]);831}832assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 2);833});834835test('positional arguments with value_hint should create input variables (GitHub issue #266106)', () => {836const manifest: IGalleryMcpServerConfiguration = {837packages: [{838registryType: RegistryType.NODE,839identifier: '@example/math-tool',840transport: { type: TransportType.STDIO },841version: '2.0.1',842packageArguments: [{843type: 'positional',844valueHint: 'calculation_type',845description: 'Type of calculation to enable',846isRequired: true,847isRepeated: false848// Note: No 'value' field, only value_hint - should create input variable849}]850}]851};852853const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);854855// BUG: Currently value_hint is used as literal value instead of creating input variable856// Should create input variable instead857assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);858assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].id, 'calculation_type');859assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].description, 'Type of calculation to enable');860assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].type, McpServerVariableType.PROMPT);861862// Args should use input variable interpolation863if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {864assert.deepStrictEqual(result.mcpServerConfiguration.config.args, [865'@example/[email protected]',866'${input:calculation_type}'867]);868}869});870});871872suite('Edge Cases and Error Handling', () => {873test('empty manifest should throw error', () => {874const manifest: IGalleryMcpServerConfiguration = {};875876assert.throws(() => {877service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);878}, /No server package found/);879});880881test('manifest with no matching package type should use first package', () => {882const manifest: IGalleryMcpServerConfiguration = {883packages: [{884registryType: RegistryType.PYTHON,885transport: { type: TransportType.STDIO },886identifier: 'python-server',887version: '1.0.0'888}]889};890891const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);892893assert.strictEqual(result.mcpServerConfiguration.config.type, McpServerType.LOCAL);894if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {895assert.strictEqual(result.mcpServerConfiguration.config.command, 'uvx'); // Python command since that's the package type896assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]']);897}898});899900test('manifest with matching package type should use that package', () => {901const manifest: IGalleryMcpServerConfiguration = {902packages: [{903registryType: RegistryType.PYTHON,904transport: { type: TransportType.STDIO },905identifier: 'python-server',906version: '1.0.0'907}, {908registryType: RegistryType.NODE,909transport: { type: TransportType.STDIO },910identifier: 'node-server',911version: '2.0.0'912}]913};914915const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);916917if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {918assert.strictEqual(result.mcpServerConfiguration.config.command, 'npx');919assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]']);920}921});922923test('undefined environment variables should be omitted', () => {924const manifest: IGalleryMcpServerConfiguration = {925packages: [{926registryType: RegistryType.NODE,927transport: { type: TransportType.STDIO },928identifier: 'test-server',929version: '1.0.0'930}]931};932933const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);934935if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {936assert.strictEqual(result.mcpServerConfiguration.config.env, undefined);937}938});939940test('named argument without value should only add name', () => {941const manifest: IGalleryMcpServerConfiguration = {942packages: [{943registryType: RegistryType.NODE,944transport: { type: TransportType.STDIO },945identifier: 'test-server',946version: '1.0.0',947runtimeArguments: [{948type: 'named',949name: '--verbose',950isRepeated: false951}]952}]953};954955const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);956957if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {958assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['--verbose', '[email protected]']);959}960});961962test('positional argument with undefined value should use value_hint', () => {963const manifest: IGalleryMcpServerConfiguration = {964packages: [{965registryType: RegistryType.NODE,966identifier: 'test-server',967transport: { type: TransportType.STDIO },968version: '1.0.0',969packageArguments: [{970type: 'positional',971valueHint: 'target_directory',972isRepeated: false973}]974}]975};976977const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);978979if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {980assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]', 'target_directory']);981}982});983984test('named argument with no name should generate notice', () => {985const manifest = {986packages: [{987registryType: RegistryType.NODE,988identifier: 'test-server',989transport: { type: TransportType.STDIO },990version: '1.0.0',991runtimeArguments: [{992type: 'named',993value: 'some-value',994isRepeated: false995}]996}]997};998999const result = service.getMcpServerConfigurationFromManifest(manifest as IGalleryMcpServerConfiguration, RegistryType.NODE);10001001// Should generate a notice about the missing name1002assert.strictEqual(result.notices.length, 1);1003assert.ok(result.notices[0].includes('Named argument is missing a name'));1004assert.ok(result.notices[0].includes('some-value')); // Should include the argument details in JSON format10051006if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {1007assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]']);1008}1009});10101011test('named argument with empty name should generate notice', () => {1012const manifest: IGalleryMcpServerConfiguration = {1013packages: [{1014registryType: RegistryType.NODE,1015identifier: 'test-server',1016transport: { type: TransportType.STDIO },1017version: '1.0.0',1018runtimeArguments: [{1019type: 'named',1020name: '',1021value: 'some-value',1022isRepeated: false1023}]1024}]1025};10261027const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);10281029// Should generate a notice about the missing name1030assert.strictEqual(result.notices.length, 1);1031assert.ok(result.notices[0].includes('Named argument is missing a name'));1032assert.ok(result.notices[0].includes('some-value')); // Should include the argument details in JSON format10331034if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {1035assert.deepStrictEqual(result.mcpServerConfiguration.config.args, ['[email protected]']);1036}1037});1038});10391040suite('Variable Processing Order', () => {1041test('should use explicit variables instead of auto-generating when both are possible', () => {1042const manifest: IGalleryMcpServerConfiguration = {1043packages: [{1044registryType: RegistryType.NODE,1045identifier: 'test-server',1046transport: { type: TransportType.STDIO },1047version: '1.0.0',1048environmentVariables: [{1049name: 'API_KEY',1050value: 'Bearer {api_key}',1051description: 'Should not be used', // This should be ignored since we have explicit variables1052variables: {1053api_key: {1054description: 'Your API key',1055isSecret: true1056}1057}1058}]1059}]1060};10611062const result = service.getMcpServerConfigurationFromManifest(manifest, RegistryType.NODE);10631064assert.strictEqual(result.mcpServerConfiguration.inputs?.length, 1);1065assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].id, 'api_key');1066assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].description, 'Your API key');1067assert.strictEqual(result.mcpServerConfiguration.inputs?.[0].password, true);10681069if (result.mcpServerConfiguration.config.type === McpServerType.LOCAL) {1070assert.strictEqual(result.mcpServerConfiguration.config.env?.['API_KEY'], 'Bearer ${input:api_key}');1071}1072});1073});1074});107510761077